From 9509b855f1049852c291f14a08c7eee5289c6817 Mon Sep 17 00:00:00 2001 From: Quentin McGaw Date: Fri, 5 Mar 2021 22:58:57 -0500 Subject: [PATCH] Feature: PrivateVPN support (#393) --- .github/labels.yml | 3 + README.md | 6 +- internal/cli/update.go | 1 + internal/configuration/openvpn.go | 6 +- internal/configuration/privatevpn.go | 52 ++++++++ internal/configuration/provider.go | 2 + internal/configuration/provider_test.go | 18 +++ internal/configuration/selection.go | 6 +- internal/configuration/updater.go | 3 + internal/constants/constants.go | 22 ++++ internal/constants/countries.go | 1 + internal/constants/privatevpn.go | 120 +++++++++++++++++ internal/constants/servers.go | 5 + internal/constants/servers_test.go | 10 ++ internal/constants/vpn.go | 2 + internal/models/server.go | 12 ++ internal/models/servers.go | 7 + internal/provider/constants.go | 1 + internal/provider/privatevpn.go | 165 ++++++++++++++++++++++++ internal/provider/provider.go | 2 + internal/storage/merge.go | 17 +++ internal/storage/sync.go | 1 + internal/updater/privatevpn.go | 133 +++++++++++++++++++ internal/updater/updater.go | 10 ++ 24 files changed, 597 insertions(+), 8 deletions(-) create mode 100644 internal/configuration/privatevpn.go create mode 100644 internal/constants/privatevpn.go create mode 100644 internal/provider/privatevpn.go create mode 100644 internal/updater/privatevpn.go 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