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" - name: ":cloud: Privado"
color: "cfe8d4" color: "cfe8d4"
description: "" description: ""
- name: ":cloud: PrivateVPN"
color: "cfe8d4"
description: ""
- name: ":cloud: PureVPN" - name: ":cloud: PureVPN"
color: "cfe8d4" color: "cfe8d4"
description: "" description: ""

View File

@@ -1,8 +1,8 @@
# Gluetun VPN client # Gluetun VPN client
*Lightweight swiss-knife-like VPN client to tunnel to Cyberghost, *Lightweight swiss-knife-like VPN client to tunnel to Cyberghost,
HideMyAss, Mullvad, NordVPN, Privado, Private Internet Access, PureVPN, HideMyAss, Mullvad, NordVPN, Privado, Private Internet Access, PrivateVPN,
Surfshark, TorGuard, VyprVPN and Windscribe VPN servers PureVPN, Surfshark, TorGuard, VyprVPN and Windscribe VPN servers
using Go, OpenVPN, iptables, DNS over TLS, ShadowSocks and an HTTP proxy* using Go, OpenVPN, iptables, DNS over TLS, ShadowSocks and an HTTP proxy*
**ANNOUNCEMENT**: *New Docker image name `qmcgaw/gluetun`* **ANNOUNCEMENT**: *New Docker image name `qmcgaw/gluetun`*
@@ -39,7 +39,7 @@ using Go, OpenVPN, iptables, DNS over TLS, ShadowSocks and an HTTP proxy*
## Features ## Features
- Based on Alpine 3.12 for a small Docker image of 52MB - 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 - Supports Openvpn only for now
- DNS over TLS baked in with service provider(s) of your choice - 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 - 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.Nordvpn, "nordvpn", false, "Update Nordvpn servers")
flagSet.BoolVar(&options.PIA, "pia", false, "Update Private Internet Access post-summer 2020 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.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.Purevpn, "purevpn", false, "Update Purevpn servers")
flagSet.BoolVar(&options.Surfshark, "surfshark", false, "Update Surfshark servers") flagSet.BoolVar(&options.Surfshark, "surfshark", false, "Update Surfshark servers")
flagSet.BoolVar(&options.Torguard, "torguard", false, "Update Torguard 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) { func (settings *OpenVPN) read(r reader) (err error) {
vpnsp, err := r.env.Inside("VPNSP", []string{ vpnsp, err := r.env.Inside("VPNSP", []string{
"cyberghost", "hidemyass", "mullvad", "nordvpn", "privado", "cyberghost", "hidemyass", "mullvad", "nordvpn", "privado",
"pia", "private internet access", "purevpn", "surfshark", "pia", "private internet access", "privatevpn",
"torguard", "vyprvpn", "windscribe"}, "purevpn", "surfshark", "torguard", "vyprvpn", "windscribe"},
params.Default("private internet access")) params.Default("private internet access"))
if err != nil { if err != nil {
return err return err
@@ -125,6 +125,8 @@ func (settings *OpenVPN) read(r reader) (err error) {
readProvider = settings.Provider.readPrivado readProvider = settings.Provider.readPrivado
case constants.PrivateInternetAccess: case constants.PrivateInternetAccess:
readProvider = settings.Provider.readPrivateInternetAccess readProvider = settings.Provider.readPrivateInternetAccess
case constants.Privatevpn:
readProvider = settings.Provider.readPrivatevpn
case constants.Purevpn: case constants.Purevpn:
readProvider = settings.Provider.readPurevpn readProvider = settings.Provider.readPurevpn
case constants.Surfshark: 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() providerLines = settings.nordvpnLines()
case "privado": case "privado":
providerLines = settings.privadoLines() providerLines = settings.privadoLines()
case "privatevpn":
providerLines = settings.privatevpnLines()
case "private internet access": case "private internet access":
providerLines = settings.privateinternetaccessLines() providerLines = settings.privateinternetaccessLines()
case "purevpn": case "purevpn":

View File

@@ -114,6 +114,24 @@ func Test_Provider_lines(t *testing.T) {
" |--Hostnames: a, b", " |--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": { "private internet access": {
settings: Provider{ settings: Provider{
Name: constants.PrivateInternetAccess, Name: constants.PrivateInternetAccess,

View File

@@ -15,9 +15,9 @@ type ServerSelection struct {
// Cyberghost // Cyberghost
Group string `json:"group"` Group string `json:"group"`
Countries []string `json:"countries"` // HideMyAss, Mullvad, PureVPN Countries []string `json:"countries"` // HideMyAss, Mullvad, PrivateVPN, PureVPN
Cities []string `json:"cities"` // HideMyAss, Mullvad, PureVPN, Windscribe Cities []string `json:"cities"` // HideMyAss, Mullvad, PrivateVPN, PureVPN, Windscribe
Hostnames []string `json:"hostnames"` // HideMyAss, Windscribe, Privado Hostnames []string `json:"hostnames"` // HideMyAss, PrivateVPN, Windscribe, Privado
// Mullvad // Mullvad
ISPs []string `json:"isps"` ISPs []string `json:"isps"`

View File

@@ -16,6 +16,7 @@ type Updater struct {
Nordvpn bool `json:"nordvpn"` Nordvpn bool `json:"nordvpn"`
PIA bool `json:"pia"` PIA bool `json:"pia"`
Privado bool `json:"privado"` Privado bool `json:"privado"`
Privatevpn bool `json:"privatevpn"`
Purevpn bool `json:"purevpn"` Purevpn bool `json:"purevpn"`
Surfshark bool `json:"surfshark"` Surfshark bool `json:"surfshark"`
Torguard bool `json:"torguard"` Torguard bool `json:"torguard"`
@@ -49,6 +50,8 @@ func (settings *Updater) read(r reader) (err error) {
settings.Nordvpn = true settings.Nordvpn = true
settings.Privado = true settings.Privado = true
settings.PIA = true settings.PIA = true
settings.Privado = true
settings.Privatevpn = true
settings.Purevpn = true settings.Purevpn = true
settings.Surfshark = true settings.Surfshark = true
settings.Torguard = true settings.Torguard = true

View File

@@ -1,3 +1,25 @@
// Package constants defines constants shared throughout the program. // Package constants defines constants shared throughout the program.
// It also defines constant maps and slices using functions. // It also defines constant maps and slices using functions.
package constants 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", "ua": "Ukraine",
"ae": "United Arab Emirates", "ae": "United Arab Emirates",
"gb": "United Kingdom", "gb": "United Kingdom",
"uk": "United Kingdom",
"um": "United States Minor Outlying Islands", "um": "United States Minor Outlying Islands",
"us": "United States", "us": "United States",
"uy": "Uruguay", "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, Timestamp: 1612031135,
Servers: PrivadoServers(), Servers: PrivadoServers(),
}, },
Privatevpn: models.PrivatevpnServers{
Version: 1,
Timestamp: 1613861528,
Servers: PrivatevpnServers(),
},
Pia: models.PiaServers{ Pia: models.PiaServers{
Version: 4, Version: 4,
Timestamp: 1613480675, Timestamp: 1613480675,

View File

@@ -64,6 +64,11 @@ func Test_versions(t *testing.T) {
version: allServers.Pia.Version, version: allServers.Pia.Version,
digest: "3e6066ec", digest: "3e6066ec",
}, },
"Privatevpn": {
model: models.PrivatevpnServer{},
version: allServers.Privatevpn.Version,
digest: "cba13d78",
},
"Purevpn": { "Purevpn": {
model: models.PurevpnServer{}, model: models.PurevpnServer{},
version: allServers.Purevpn.Version, version: allServers.Purevpn.Version,
@@ -155,6 +160,11 @@ func Test_timestamps(t *testing.T) {
timestamp: allServers.Pia.Timestamp, timestamp: allServers.Pia.Timestamp,
digest: "e0f95a01", digest: "e0f95a01",
}, },
"Privatevpn": {
servers: allServers.Privatevpn.Servers,
timestamp: allServers.Privatevpn.Timestamp,
digest: "8ce3fba1",
},
"Purevpn": { "Purevpn": {
servers: allServers.Purevpn.Servers, servers: allServers.Purevpn.Servers,
timestamp: allServers.Purevpn.Timestamp, timestamp: allServers.Purevpn.Timestamp,

View File

@@ -13,6 +13,8 @@ const (
Privado = "privado" Privado = "privado"
// PrivateInternetAccess is a VPN provider. // PrivateInternetAccess is a VPN provider.
PrivateInternetAccess = "private internet access" PrivateInternetAccess = "private internet access"
// Privatevpn is a VPN provider.
Privatevpn = "privatevpn"
// PureVPN is a VPN provider. // PureVPN is a VPN provider.
Purevpn = "purevpn" Purevpn = "purevpn"
// Surfshark is a VPN provider. // 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)) 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 { func goStringifyIP(ip net.IP) string {
s := fmt.Sprintf("%#v", ip) s := fmt.Sprintf("%#v", ip)
s = strings.TrimSuffix(strings.TrimPrefix(s, "net.IP{"), "}") s = strings.TrimSuffix(strings.TrimPrefix(s, "net.IP{"), "}")

View File

@@ -8,6 +8,7 @@ type AllServers struct {
Nordvpn NordvpnServers `json:"nordvpn"` Nordvpn NordvpnServers `json:"nordvpn"`
Privado PrivadoServers `json:"privado"` Privado PrivadoServers `json:"privado"`
Pia PiaServers `json:"pia"` Pia PiaServers `json:"pia"`
Privatevpn PrivatevpnServers `json:"privatevpn"`
Purevpn PurevpnServers `json:"purevpn"` Purevpn PurevpnServers `json:"purevpn"`
Surfshark SurfsharkServers `json:"surfshark"` Surfshark SurfsharkServers `json:"surfshark"`
Torguard TorguardServers `json:"torguard"` Torguard TorguardServers `json:"torguard"`
@@ -22,6 +23,7 @@ func (a *AllServers) Count() int {
len(a.Nordvpn.Servers) + len(a.Nordvpn.Servers) +
len(a.Privado.Servers) + len(a.Privado.Servers) +
len(a.Pia.Servers) + len(a.Pia.Servers) +
len(a.Privatevpn.Servers) +
len(a.Purevpn.Servers) + len(a.Purevpn.Servers) +
len(a.Surfshark.Servers) + len(a.Surfshark.Servers) +
len(a.Torguard.Servers) + len(a.Torguard.Servers) +
@@ -59,6 +61,11 @@ type PiaServers struct {
Timestamp int64 `json:"timestamp"` Timestamp int64 `json:"timestamp"`
Servers []PIAServer `json:"servers"` Servers []PIAServer `json:"servers"`
} }
type PrivatevpnServers struct {
Version uint16 `json:"version"`
Timestamp int64 `json:"timestamp"`
Servers []PrivatevpnServer `json:"servers"`
}
type PurevpnServers struct { type PurevpnServers struct {
Version uint16 `json:"version"` Version uint16 `json:"version"`
Timestamp int64 `json:"timestamp"` Timestamp int64 `json:"timestamp"`

View File

@@ -2,6 +2,7 @@ package provider
const ( const (
aes256cbc = "aes-256-cbc" aes256cbc = "aes-256-cbc"
aes128gcm = "aes-128-gcm"
aes256gcm = "aes-256-gcm" aes256gcm = "aes-256-gcm"
sha256 = "sha256" 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) return newPrivado(allServers.Privado.Servers, timeNow)
case constants.PrivateInternetAccess: case constants.PrivateInternetAccess:
return newPrivateInternetAccess(allServers.Pia.Servers, timeNow) return newPrivateInternetAccess(allServers.Pia.Servers, timeNow)
case constants.Privatevpn:
return newPrivatevpn(allServers.Privatevpn.Servers, timeNow)
case constants.Purevpn: case constants.Purevpn:
return newPurevpn(allServers.Purevpn.Servers, timeNow) return newPurevpn(allServers.Purevpn.Servers, timeNow)
case constants.Surfshark: 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), Nordvpn: s.mergeNordVPN(hardcoded.Nordvpn, persisted.Nordvpn),
Privado: s.mergePrivado(hardcoded.Privado, persisted.Privado), Privado: s.mergePrivado(hardcoded.Privado, persisted.Privado),
Pia: s.mergePIA(hardcoded.Pia, persisted.Pia), Pia: s.mergePIA(hardcoded.Pia, persisted.Pia),
Privatevpn: s.mergePrivatevpn(hardcoded.Privatevpn, persisted.Privatevpn),
Purevpn: s.mergePureVPN(hardcoded.Purevpn, persisted.Purevpn), Purevpn: s.mergePureVPN(hardcoded.Purevpn, persisted.Purevpn),
Surfshark: s.mergeSurfshark(hardcoded.Surfshark, persisted.Surfshark), Surfshark: s.mergeSurfshark(hardcoded.Surfshark, persisted.Surfshark),
Torguard: s.mergeTorguard(hardcoded.Torguard, persisted.Torguard), Torguard: s.mergeTorguard(hardcoded.Torguard, persisted.Torguard),
@@ -106,6 +107,22 @@ func (s *storage) mergePIA(hardcoded, persisted models.PiaServers) models.PiaSer
return persisted 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 { func (s *storage) mergePureVPN(hardcoded, persisted models.PurevpnServers) models.PurevpnServers {
if persisted.Timestamp <= hardcoded.Timestamp { if persisted.Timestamp <= hardcoded.Timestamp {
return hardcoded return hardcoded

View File

@@ -23,6 +23,7 @@ func countServers(allServers models.AllServers) int {
len(allServers.Nordvpn.Servers) + len(allServers.Nordvpn.Servers) +
len(allServers.Privado.Servers) + len(allServers.Privado.Servers) +
len(allServers.Pia.Servers) + len(allServers.Pia.Servers) +
len(allServers.Privatevpn.Servers) +
len(allServers.Purevpn.Servers) + len(allServers.Purevpn.Servers) +
len(allServers.Surfshark.Servers) + len(allServers.Surfshark.Servers) +
len(allServers.Torguard.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 { if u.options.Purevpn {
u.logger.Info("updating PureVPN servers...") u.logger.Info("updating PureVPN servers...")
// TODO support servers offering only TCP or only UDP // TODO support servers offering only TCP or only UDP