From e0e3ca3832fb647944a9849ce2ff69e9e8ad2169 Mon Sep 17 00:00:00 2001 From: Quentin McGaw Date: Tue, 5 Oct 2021 10:44:15 -0700 Subject: [PATCH] Feat: Perfect privacy support (#606) --- .github/labels.yml | 3 + README.md | 4 +- internal/cli/formatservers.go | 5 +- internal/cli/update.go | 1 + internal/configuration/perfectprivacy.go | 43 ++ internal/configuration/provider.go | 4 +- internal/configuration/provider_test.go | 15 + internal/configuration/selection.go | 3 +- internal/configuration/updater.go | 44 +- internal/constants/perfectprivacy.go | 21 + internal/constants/vpn.go | 2 + internal/models/getservers.go | 13 + internal/models/getservers_test.go | 5 + internal/models/markdown.go | 13 + internal/models/server.go | 7 + internal/models/servers.go | 47 +- internal/openvpn/parse/pem.go | 2 - internal/openvpn/parse/pem_test.go | 2 +- .../provider/perfectprivacy/connection.go | 37 ++ internal/provider/perfectprivacy/filter.go | 26 ++ .../provider/perfectprivacy/openvpnconf.go | 89 ++++ internal/provider/perfectprivacy/provider.go | 23 + internal/provider/provider.go | 3 + internal/storage/hardcoded_test.go | 5 + internal/storage/merge.go | 50 ++- internal/storage/read.go | 91 ++-- internal/storage/read_test.go | 119 ++--- internal/storage/servers.json | 417 +++++++++++++++++- internal/storage/sync.go | 1 + internal/updater/openvpn/extract.go | 37 +- internal/updater/providers.go | 22 + .../providers/perfectprivacy/citytoserver.go | 45 ++ .../providers/perfectprivacy/filename.go | 20 + .../providers/perfectprivacy/servers.go | 73 +++ .../updater/providers/perfectprivacy/sort.go | 13 + internal/updater/unzip/extract.go | 3 +- internal/updater/unzip/fetch.go | 12 +- internal/updater/updater.go | 10 + 38 files changed, 1142 insertions(+), 188 deletions(-) create mode 100644 internal/configuration/perfectprivacy.go create mode 100644 internal/constants/perfectprivacy.go create mode 100644 internal/provider/perfectprivacy/connection.go create mode 100644 internal/provider/perfectprivacy/filter.go create mode 100644 internal/provider/perfectprivacy/openvpnconf.go create mode 100644 internal/provider/perfectprivacy/provider.go create mode 100644 internal/updater/providers/perfectprivacy/citytoserver.go create mode 100644 internal/updater/providers/perfectprivacy/filename.go create mode 100644 internal/updater/providers/perfectprivacy/servers.go create mode 100644 internal/updater/providers/perfectprivacy/sort.go diff --git a/.github/labels.yml b/.github/labels.yml index 5adb93ac..3a20030f 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -39,6 +39,9 @@ - name: ":cloud: NordVPN" color: "cfe8d4" description: "" +- name: ":cloud: Perfect Privacy" + color: "cfe8d4" + description: "" - name: ":cloud: PIA" color: "cfe8d4" description: "" diff --git a/README.md b/README.md index 376d511a..69c28c4a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Gluetun VPN client *Lightweight swiss-knife-like VPN client to tunnel to Cyberghost, ExpressVPN, FastestVPN, -HideMyAss, IPVanish, IVPN, Mullvad, NordVPN, Privado, Private Internet Access, PrivateVPN, +HideMyAss, IPVanish, IVPN, Mullvad, NordVPN, Perfect Privacy, Privado, Private Internet Access, PrivateVPN, ProtonVPN, PureVPN, Surfshark, TorGuard, VPNUnlimited, VyprVPN, WeVPN and Windscribe VPN servers using Go, OpenVPN or Wireguard, iptables, DNS over TLS, ShadowSocks and an HTTP proxy* @@ -62,7 +62,7 @@ using Go, OpenVPN or Wireguard, iptables, DNS over TLS, ShadowSocks and an HTTP ## Features - Based on Alpine 3.14 for a small Docker image of 31MB -- Supports: **Cyberghost**, **ExpressVPN**, **FastestVPN**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **Surfshark**, **TorGuard**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers +- Supports: **Cyberghost**, **ExpressVPN**, **FastestVPN**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **Surfshark**, **TorGuard**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers - Supports OpenVPN for all providers listed - Supports Wireguard - For **Mullvad**, **Ivpn** and **Windscribe** diff --git a/internal/cli/formatservers.go b/internal/cli/formatservers.go index a0aba335..b4654403 100644 --- a/internal/cli/formatservers.go +++ b/internal/cli/formatservers.go @@ -26,7 +26,7 @@ var ( func (c *CLI) FormatServers(args []string) error { var format, output string var cyberghost, expressvpn, fastestvpn, hideMyAss, ipvanish, ivpn, mullvad, - nordvpn, pia, privado, privatevpn, protonvpn, purevpn, surfshark, + nordvpn, perfectPrivacy, pia, privado, privatevpn, protonvpn, purevpn, surfshark, torguard, vpnUnlimited, vyprvpn, wevpn, windscribe bool flagSet := flag.NewFlagSet("markdown", flag.ExitOnError) flagSet.StringVar(&format, "format", "markdown", "Format to use which can be: 'markdown'") @@ -39,6 +39,7 @@ func (c *CLI) FormatServers(args []string) error { flagSet.BoolVar(&ivpn, "ivpn", false, "Format IVPN servers") flagSet.BoolVar(&mullvad, "mullvad", false, "Format Mullvad servers") flagSet.BoolVar(&nordvpn, "nordvpn", false, "Format Nordvpn servers") + flagSet.BoolVar(&perfectPrivacy, "perfectprivacy", false, "Format Perfect Privacy servers") flagSet.BoolVar(&pia, "pia", false, "Format Private Internet Access servers") flagSet.BoolVar(&privado, "privado", false, "Format Privado servers") flagSet.BoolVar(&privatevpn, "privatevpn", false, "Format Private VPN servers") @@ -83,6 +84,8 @@ func (c *CLI) FormatServers(args []string) error { formatted = currentServers.Mullvad.ToMarkdown() case nordvpn: formatted = currentServers.Nordvpn.ToMarkdown() + case perfectPrivacy: + formatted = currentServers.Perfectprivacy.ToMarkdown() case pia: formatted = currentServers.Pia.ToMarkdown() case privado: diff --git a/internal/cli/update.go b/internal/cli/update.go index 682fa9ea..899f08d7 100644 --- a/internal/cli/update.go +++ b/internal/cli/update.go @@ -51,6 +51,7 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e flagSet.BoolVar(&options.Ivpn, "ivpn", false, "Update IVPN servers") flagSet.BoolVar(&options.Mullvad, "mullvad", false, "Update Mullvad servers") flagSet.BoolVar(&options.Nordvpn, "nordvpn", false, "Update Nordvpn servers") + flagSet.BoolVar(&options.Perfectprivacy, "perfectprivacy", false, "Update Perfect Privacy 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") diff --git a/internal/configuration/perfectprivacy.go b/internal/configuration/perfectprivacy.go new file mode 100644 index 00000000..51af4dcd --- /dev/null +++ b/internal/configuration/perfectprivacy.go @@ -0,0 +1,43 @@ +package configuration + +import ( + "fmt" + + "github.com/qdm12/gluetun/internal/constants" +) + +func (settings *Provider) readPerfectPrivacy(r reader) (err error) { + settings.Name = constants.Perfectprivacy + servers := r.servers.GetPerfectprivacy() + + settings.ServerSelection.TargetIP, err = readTargetIP(r.env) + if err != nil { + return err + } + + settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.PerfectprivacyCityChoices(servers)) + if err != nil { + return fmt.Errorf("environment variable CITY: %w", err) + } + + return settings.ServerSelection.OpenVPN.readPerfectPrivacy(r) +} + +func (settings *OpenVPNSelection) readPerfectPrivacy(r reader) (err error) { + settings.TCP, err = readOpenVPNProtocol(r) + if err != nil { + return err + } + + portValidation := openvpnPortValidation{ + tcp: settings.TCP, + allowedTCP: []uint16{44, 443, 4433}, + allowedUDP: []uint16{44, 443, 4433}, + } + settings.CustomPort, err = readOpenVPNCustomPort(r, portValidation) + if err != nil { + return err + } + + return nil +} diff --git a/internal/configuration/provider.go b/internal/configuration/provider.go index b39fac52..d4bd2a4e 100644 --- a/internal/configuration/provider.go +++ b/internal/configuration/provider.go @@ -67,6 +67,8 @@ func (settings *Provider) read(r reader, vpnType string) error { err = settings.readMullvad(r) case constants.Nordvpn: err = settings.readNordvpn(r) + case constants.Perfectprivacy: + err = settings.readPerfectPrivacy(r) case constants.Privado: err = settings.readPrivado(r) case constants.PrivateInternetAccess: @@ -108,7 +110,7 @@ func (settings *Provider) readVPNServiceProvider(r reader, vpnType string) (err constants.Custom, "cyberghost", constants.Expressvpn, "fastestvpn", "hidemyass", "ipvanish", "ivpn", "mullvad", "nordvpn", - "privado", "pia", "private internet access", "privatevpn", "protonvpn", + constants.Perfectprivacy, "privado", "pia", "private internet access", "privatevpn", "protonvpn", "purevpn", "surfshark", "torguard", constants.VPNUnlimited, "vyprvpn", constants.Wevpn, "windscribe"} case constants.Wireguard: diff --git a/internal/configuration/provider_test.go b/internal/configuration/provider_test.go index f6a6a7c2..79b1782f 100644 --- a/internal/configuration/provider_test.go +++ b/internal/configuration/provider_test.go @@ -168,6 +168,21 @@ func Test_Provider_lines(t *testing.T) { " |--Protocol: udp", }, }, + "perfectprivacy": { + settings: Provider{ + Name: constants.Perfectprivacy, + ServerSelection: ServerSelection{ + VPN: constants.OpenVPN, + Cities: []string{"a", "b"}, + }, + }, + lines: []string{ + "|--Perfect Privacy settings:", + " |--Cities: a, b", + " |--OpenVPN selection:", + " |--Protocol: udp", + }, + }, "privado": { settings: Provider{ Name: constants.Privado, diff --git a/internal/configuration/selection.go b/internal/configuration/selection.go index d34b0445..e2318e30 100644 --- a/internal/configuration/selection.go +++ b/internal/configuration/selection.go @@ -17,7 +17,8 @@ type ServerSelection struct { //nolint:maligned // Expressvpn, Fastestvpn, HideMyAss, IPVanish, IVPN, Mullvad, PrivateVPN, Protonvpn, PureVPN, VPNUnlimited Countries []string `json:"countries"` - // Expressvpn, HideMyAss, IPVanish, IVPN, Mullvad, PrivateVPN, Protonvpn, PureVPN, VPNUnlimited, WeVPN, Windscribe + // Expressvpn, HideMyAss, IPVanish, IVPN, Mullvad, Perfectprivacy, PrivateVPN, Protonvpn, + // PureVPN, VPNUnlimited, WeVPN, Windscribe Cities []string `json:"cities"` // Expressvpn, Fastestvpn, HideMyAss, IPVanish, IVPN, PrivateVPN, Windscribe, Privado, Protonvpn, VPNUnlimited, WeVPN Hostnames []string `json:"hostnames"` diff --git a/internal/configuration/updater.go b/internal/configuration/updater.go index 340bfe36..6ded2a17 100644 --- a/internal/configuration/updater.go +++ b/internal/configuration/updater.go @@ -9,27 +9,28 @@ import ( ) type Updater struct { - Period time.Duration `json:"period"` - DNSAddress string `json:"dns_address"` - Cyberghost bool `json:"cyberghost"` - Expressvpn bool `json:"expressvpn"` - Fastestvpn bool `json:"fastestvpn"` - HideMyAss bool `json:"hidemyass"` - Ipvanish bool `json:"ipvanish"` - Ivpn bool `json:"ivpn"` - Mullvad bool `json:"mullvad"` - Nordvpn bool `json:"nordvpn"` - PIA bool `json:"pia"` - Privado bool `json:"privado"` - Privatevpn bool `json:"privatevpn"` - Protonvpn bool `json:"protonvpn"` - Purevpn bool `json:"purevpn"` - Surfshark bool `json:"surfshark"` - Torguard bool `json:"torguard"` - VPNUnlimited bool `json:"vpnunlimited"` - Vyprvpn bool `json:"vyprvpn"` - Wevpn bool `json:"wevpn"` - Windscribe bool `json:"windscribe"` + Period time.Duration `json:"period"` + DNSAddress string `json:"dns_address"` + Cyberghost bool `json:"cyberghost"` + Expressvpn bool `json:"expressvpn"` + Fastestvpn bool `json:"fastestvpn"` + HideMyAss bool `json:"hidemyass"` + Ipvanish bool `json:"ipvanish"` + Ivpn bool `json:"ivpn"` + Mullvad bool `json:"mullvad"` + Nordvpn bool `json:"nordvpn"` + Perfectprivacy bool `json:"perfectprivacy"` + PIA bool `json:"pia"` + Privado bool `json:"privado"` + Privatevpn bool `json:"privatevpn"` + Protonvpn bool `json:"protonvpn"` + Purevpn bool `json:"purevpn"` + Surfshark bool `json:"surfshark"` + Torguard bool `json:"torguard"` + VPNUnlimited bool `json:"vpnunlimited"` + Vyprvpn bool `json:"vyprvpn"` + Wevpn bool `json:"wevpn"` + Windscribe bool `json:"windscribe"` // The two below should be used in CLI mode only CLI bool `json:"-"` } @@ -57,6 +58,7 @@ func (settings *Updater) EnableAll() { settings.Ivpn = true settings.Mullvad = true settings.Nordvpn = true + settings.Perfectprivacy = true settings.Privado = true settings.PIA = true settings.Privado = true diff --git a/internal/constants/perfectprivacy.go b/internal/constants/perfectprivacy.go new file mode 100644 index 00000000..baa0aba6 --- /dev/null +++ b/internal/constants/perfectprivacy.go @@ -0,0 +1,21 @@ +package constants + +import ( + "github.com/qdm12/gluetun/internal/models" +) + +//nolint:lll +const ( + PerfectprivacyCA = "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" + PerfectprivacyCert = "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" + PerfectprivacyPrivateKey = "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" + PerfectprivacyTLSCryptOpenvpnStaticKeyV1 = "d10a8e2641f5834f6c5e04a6ee9a798553d338fa2836ef2a91057c1f6174a3a12b36f16d1110b20e42ae94d3bd579213e9c3770be6c74804348dddba876945a5a3ab7660f9436f85f331641f6efc81315f0d12b2766a9f15c10a53cf9ba32dc80f03b5f15a6cc6987bda795dbe83443ec81f3d5e161cd47fab6b1f125b3adeee1eae33370d018594e0ff6b25b815228d27371b32c82a95f4929d3abb5fa36e57bf1f42353542568fbb8233f4645f05820275f79570cb8bbcf8010fc5d20f07d031a8227d45daf7349e34158c91a3d4e5add19cfa02f683f87609f6525fa0594016d11abf2de649f83ad54edd3e74e032e34b1bca685b8499916826d9aee11c13" +) + +func PerfectprivacyCityChoices(servers []models.PerfectprivacyServer) (choices []string) { + choices = make([]string, len(servers)) + for i := range servers { + choices[i] = servers[i].City + } + return makeUnique(choices) +} diff --git a/internal/constants/vpn.go b/internal/constants/vpn.go index ed9eb3c1..3abc5a22 100644 --- a/internal/constants/vpn.go +++ b/internal/constants/vpn.go @@ -25,6 +25,8 @@ const ( Mullvad = "mullvad" // Nordvpn is a VPN provider. Nordvpn = "nordvpn" + // Perfectprivacy is a VPN provider. + Perfectprivacy = "perfect privacy" // Privado is a VPN provider. Privado = "privado" // PrivateInternetAccess is a VPN provider. diff --git a/internal/models/getservers.go b/internal/models/getservers.go index 2b9922b1..2bb91964 100644 --- a/internal/models/getservers.go +++ b/internal/models/getservers.go @@ -14,6 +14,7 @@ func (a AllServers) GetCopy() (servers AllServers) { servers.Ivpn.Servers = a.GetIvpn() servers.Mullvad.Servers = a.GetMullvad() servers.Nordvpn.Servers = a.GetNordvpn() + servers.Perfectprivacy.Servers = a.GetPerfectprivacy() servers.Privado.Servers = a.GetPrivado() servers.Pia.Servers = a.GetPia() servers.Privatevpn.Servers = a.GetPrivatevpn() @@ -124,6 +125,18 @@ func (a *AllServers) GetNordvpn() (servers []NordvpnServer) { return servers } +func (a *AllServers) GetPerfectprivacy() (servers []PerfectprivacyServer) { + if a.Perfectprivacy.Servers == nil { + return nil + } + servers = make([]PerfectprivacyServer, len(a.Perfectprivacy.Servers)) + for i, serverToCopy := range a.Perfectprivacy.Servers { + servers[i] = serverToCopy + servers[i].IPs = copyIPs(serverToCopy.IPs) + } + return servers +} + func (a *AllServers) GetPia() (servers []PIAServer) { if a.Pia.Servers == nil { return nil diff --git a/internal/models/getservers_test.go b/internal/models/getservers_test.go index 1cacf391..3c6dff3a 100644 --- a/internal/models/getservers_test.go +++ b/internal/models/getservers_test.go @@ -51,6 +51,11 @@ func Test_AllServers_GetCopy(t *testing.T) { IP: net.IP{1, 2, 3, 4}, }}, }, + Perfectprivacy: PerfectprivacyServers{ + Servers: []PerfectprivacyServer{{ + IPs: []net.IP{{1, 2, 3, 4}}, + }}, + }, Privado: PrivadoServers{ Servers: []PrivadoServer{{ IP: net.IP{1, 2, 3, 4}, diff --git a/internal/models/markdown.go b/internal/models/markdown.go index 47923615..201595ce 100644 --- a/internal/models/markdown.go +++ b/internal/models/markdown.go @@ -141,6 +141,19 @@ func (s *PrivadoServer) ToMarkdown() (markdown string) { s.Country, s.Region, s.City, s.Hostname) } +func (s *PerfectprivacyServers) ToMarkdown() (markdown string) { + markdown = markdownTableHeading("City", "TCP", "UDP") + for _, server := range s.Servers { + markdown += server.ToMarkdown() + "\n" + } + return markdown +} + +func (s *PerfectprivacyServer) ToMarkdown() (markdown string) { + return fmt.Sprintf("| %s | %s | %s |", + s.City, boolToMarkdown(s.TCP), boolToMarkdown(s.UDP)) +} + func (s *PiaServers) ToMarkdown() (markdown string) { markdown = markdownTableHeading("Region", "Hostname", "TCP", "UDP") for _, server := range s.Servers { diff --git a/internal/models/server.go b/internal/models/server.go index e4e55374..516eac83 100644 --- a/internal/models/server.go +++ b/internal/models/server.go @@ -81,6 +81,13 @@ type NordvpnServer struct { //nolint:maligned UDP bool `json:"udp"` } +type PerfectprivacyServer struct { + City string `json:"city"` // primary key + IPs []net.IP `json:"ips"` + TCP bool `json:"tcp"` + UDP bool `json:"udp"` +} + type PrivadoServer struct { Country string `json:"country"` Region string `json:"region"` diff --git a/internal/models/servers.go b/internal/models/servers.go index 665bfdf7..dcbed5a3 100644 --- a/internal/models/servers.go +++ b/internal/models/servers.go @@ -1,26 +1,27 @@ package models type AllServers struct { - Version uint16 `json:"version"` // used for migration of the top level scheme - Cyberghost CyberghostServers `json:"cyberghost"` - Expressvpn ExpressvpnServers `json:"expressvpn"` - Fastestvpn FastestvpnServers `json:"fastestvpn"` - HideMyAss HideMyAssServers `json:"hidemyass"` - Ipvanish IpvanishServers `json:"ipvanish"` - Ivpn IvpnServers `json:"ivpn"` - Mullvad MullvadServers `json:"mullvad"` - Nordvpn NordvpnServers `json:"nordvpn"` - Privado PrivadoServers `json:"privado"` - Pia PiaServers `json:"pia"` - Privatevpn PrivatevpnServers `json:"privatevpn"` - Protonvpn ProtonvpnServers `json:"protonvpn"` - Purevpn PurevpnServers `json:"purevpn"` - Surfshark SurfsharkServers `json:"surfshark"` - Torguard TorguardServers `json:"torguard"` - VPNUnlimited VPNUnlimitedServers `json:"vpnunlimited"` - Vyprvpn VyprvpnServers `json:"vyprvpn"` - Wevpn WevpnServers `json:"wevpn"` - Windscribe WindscribeServers `json:"windscribe"` + Version uint16 `json:"version"` // used for migration of the top level scheme + Cyberghost CyberghostServers `json:"cyberghost"` + Expressvpn ExpressvpnServers `json:"expressvpn"` + Fastestvpn FastestvpnServers `json:"fastestvpn"` + HideMyAss HideMyAssServers `json:"hidemyass"` + Ipvanish IpvanishServers `json:"ipvanish"` + Ivpn IvpnServers `json:"ivpn"` + Mullvad MullvadServers `json:"mullvad"` + Perfectprivacy PerfectprivacyServers `json:"perfectprivacy"` + Nordvpn NordvpnServers `json:"nordvpn"` + Privado PrivadoServers `json:"privado"` + Pia PiaServers `json:"pia"` + Privatevpn PrivatevpnServers `json:"privatevpn"` + Protonvpn ProtonvpnServers `json:"protonvpn"` + Purevpn PurevpnServers `json:"purevpn"` + Surfshark SurfsharkServers `json:"surfshark"` + Torguard TorguardServers `json:"torguard"` + VPNUnlimited VPNUnlimitedServers `json:"vpnunlimited"` + Vyprvpn VyprvpnServers `json:"vyprvpn"` + Wevpn WevpnServers `json:"wevpn"` + Windscribe WindscribeServers `json:"windscribe"` } func (a *AllServers) Count() int { @@ -32,6 +33,7 @@ func (a *AllServers) Count() int { len(a.Ivpn.Servers) + len(a.Mullvad.Servers) + len(a.Nordvpn.Servers) + + len(a.Perfectprivacy.Servers) + len(a.Privado.Servers) + len(a.Pia.Servers) + len(a.Privatevpn.Servers) + @@ -85,6 +87,11 @@ type NordvpnServers struct { Timestamp int64 `json:"timestamp"` Servers []NordvpnServer `json:"servers"` } +type PerfectprivacyServers struct { + Version uint16 `json:"version"` + Timestamp int64 `json:"timestamp"` + Servers []PerfectprivacyServer `json:"servers"` +} type PrivadoServers struct { Version uint16 `json:"version"` Timestamp int64 `json:"timestamp"` diff --git a/internal/openvpn/parse/pem.go b/internal/openvpn/parse/pem.go index 8a2d3fe7..cf3a9498 100644 --- a/internal/openvpn/parse/pem.go +++ b/internal/openvpn/parse/pem.go @@ -11,8 +11,6 @@ var ( ) func extractPEM(b []byte, name string) (encodedData string, err error) { - name = strings.ToUpper(name) // certificate -> CERTIFICATE - pemBlock, _ := pem.Decode(b) if pemBlock == nil { return "", errPEMDecode diff --git a/internal/openvpn/parse/pem_test.go b/internal/openvpn/parse/pem_test.go index 72832d11..b0885986 100644 --- a/internal/openvpn/parse/pem_test.go +++ b/internal/openvpn/parse/pem_test.go @@ -25,7 +25,7 @@ func Test_extractPEM(t *testing.T) { err: errors.New("cannot decode PEM encoded block"), }, "valid data": { - name: "certificate", + name: "CERTIFICATE", b: []byte(validCertPEM), encodedData: validCertData, }, diff --git a/internal/provider/perfectprivacy/connection.go b/internal/provider/perfectprivacy/connection.go new file mode 100644 index 00000000..fb464b90 --- /dev/null +++ b/internal/provider/perfectprivacy/connection.go @@ -0,0 +1,37 @@ +package perfectprivacy + +import ( + "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/models" + "github.com/qdm12/gluetun/internal/provider/utils" +) + +func (p *Perfectprivacy) GetConnection(selection configuration.ServerSelection) ( + connection models.Connection, err error) { + const defaultPort uint16 = 443 + port := defaultPort + if selection.OpenVPN.CustomPort > 0 { + port = selection.OpenVPN.CustomPort + } + protocol := utils.GetProtocol(selection) + + servers, err := p.filterServers(selection) + if err != nil { + return connection, err + } + + var connections []models.Connection + for _, server := range servers { + for _, IP := range server.IPs { + connection := models.Connection{ + Type: selection.VPN, + IP: IP, + Port: port, + Protocol: protocol, + } + connections = append(connections, connection) + } + } + + return utils.PickConnection(connections, selection, p.randSource) +} diff --git a/internal/provider/perfectprivacy/filter.go b/internal/provider/perfectprivacy/filter.go new file mode 100644 index 00000000..b95afa52 --- /dev/null +++ b/internal/provider/perfectprivacy/filter.go @@ -0,0 +1,26 @@ +package perfectprivacy + +import ( + "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/models" + "github.com/qdm12/gluetun/internal/provider/utils" +) + +func (p *Perfectprivacy) filterServers(selection configuration.ServerSelection) ( + servers []models.PerfectprivacyServer, err error) { + for _, server := range p.servers { + switch { + case + utils.FilterByPossibilities(server.City, selection.Cities), + utils.FilterByProtocol(selection, server.TCP, server.UDP): + default: + servers = append(servers, server) + } + } + + if len(servers) == 0 { + return nil, utils.NoServerFoundError(selection) + } + + return servers, nil +} diff --git a/internal/provider/perfectprivacy/openvpnconf.go b/internal/provider/perfectprivacy/openvpnconf.go new file mode 100644 index 00000000..fc054ddd --- /dev/null +++ b/internal/provider/perfectprivacy/openvpnconf.go @@ -0,0 +1,89 @@ +package perfectprivacy + +import ( + "strconv" + + "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/constants" + "github.com/qdm12/gluetun/internal/models" + "github.com/qdm12/gluetun/internal/provider/utils" +) + +func (p *Perfectprivacy) BuildConf(connection models.Connection, + settings configuration.OpenVPN) (lines []string, err error) { + if settings.Cipher == "" { + // TODO add AES 256 GCM + settings.Cipher = constants.AES256cbc + } + + if settings.Auth == "" { + settings.Auth = constants.SHA512 + } + + if settings.MSSFix == 0 { + settings.MSSFix = 1450 + } + + lines = []string{ + "client", + "nobind", + "tls-exit", + "dev " + settings.Interface, + "verb " + strconv.Itoa(settings.Verbosity), + + // Perfect Privacy specific + "ping 5", + "tun-mtu 1500", + "tun-mtu-extra 32", + "mssfix " + strconv.Itoa(int(settings.MSSFix)), + "reneg-sec 3600", + "key-direction 1", + "tls-cipher TLS_CHACHA20_POLY1305_SHA256:TLS-DHE-RSA-WITH-AES-256-GCM-SHA384:TLS-DHE-RSA-WITH-AES-256-CBC-SHA:TLS-DHE-RSA-WITH-AES-128-GCM-SHA256:TLS-DHE-RSA-WITH-AES-128-CBC-SHA:TLS_AES_256_GCM_SHA384:TLS-RSA-WITH-AES-256-CBC-SHA", //nolint:lll + "auth-user-pass " + constants.OpenVPNAuthConf, + "auth " + settings.Auth, + + // Added constant values + "auth-nocache", + "mute-replay-warnings", + "pull-filter ignore \"auth-token\"", // prevent auth failed loops + "auth-retry nointeract", + "suppress-timestamps", + + // Modified variables + connection.OpenVPNProtoLine(), + connection.OpenVPNRemoteLine(), + } + + lines = append(lines, utils.CipherLines(settings.Cipher, settings.Version)...) + + if connection.Protocol == constants.UDP { + lines = append(lines, "explicit-exit-notify") + } + + if !settings.Root { + lines = append(lines, "user "+settings.ProcUser) + lines = append(lines, "persist-tun") + lines = append(lines, "persist-key") + } + + if !settings.IPv6 { + lines = append(lines, `pull-filter ignore "route-ipv6"`) + lines = append(lines, `pull-filter ignore "ifconfig-ipv6"`) + // Perfect Privacy specific IPv6 + lines = append(lines, "redirect-gateway def1") + lines = append(lines, `pull-filter ignore "redirect-gateway def1 ipv6"`) + } + + lines = append(lines, utils.WrapOpenvpnCA( + constants.PerfectprivacyCA)...) + lines = append(lines, utils.WrapOpenvpnCert( + constants.PerfectprivacyCert)...) + lines = append(lines, utils.WrapOpenvpnKey( + constants.PerfectprivacyPrivateKey)...) + lines = append(lines, utils.WrapOpenvpnTLSCrypt( + constants.PerfectprivacyTLSCryptOpenvpnStaticKeyV1)...) + + lines = append(lines, "") + + return lines, nil +} diff --git a/internal/provider/perfectprivacy/provider.go b/internal/provider/perfectprivacy/provider.go new file mode 100644 index 00000000..6de0b2ad --- /dev/null +++ b/internal/provider/perfectprivacy/provider.go @@ -0,0 +1,23 @@ +package perfectprivacy + +import ( + "math/rand" + + "github.com/qdm12/gluetun/internal/constants" + "github.com/qdm12/gluetun/internal/models" + "github.com/qdm12/gluetun/internal/provider/utils" +) + +type Perfectprivacy struct { + servers []models.PerfectprivacyServer + randSource rand.Source + utils.NoPortForwarder +} + +func New(servers []models.PerfectprivacyServer, randSource rand.Source) *Perfectprivacy { + return &Perfectprivacy{ + servers: servers, + randSource: randSource, + NoPortForwarder: utils.NewNoPortForwarding(constants.Perfectprivacy), + } +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 6ecd1f1e..b630ea59 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -20,6 +20,7 @@ import ( "github.com/qdm12/gluetun/internal/provider/ivpn" "github.com/qdm12/gluetun/internal/provider/mullvad" "github.com/qdm12/gluetun/internal/provider/nordvpn" + "github.com/qdm12/gluetun/internal/provider/perfectprivacy" "github.com/qdm12/gluetun/internal/provider/privado" "github.com/qdm12/gluetun/internal/provider/privateinternetaccess" "github.com/qdm12/gluetun/internal/provider/privatevpn" @@ -70,6 +71,8 @@ func New(provider string, allServers models.AllServers, timeNow func() time.Time return mullvad.New(allServers.Mullvad.Servers, randSource) case constants.Nordvpn: return nordvpn.New(allServers.Nordvpn.Servers, randSource) + case constants.Perfectprivacy: + return perfectprivacy.New(allServers.Perfectprivacy.Servers, randSource) case constants.Privado: return privado.New(allServers.Privado.Servers, randSource) case constants.PrivateInternetAccess: diff --git a/internal/storage/hardcoded_test.go b/internal/storage/hardcoded_test.go index 124ee550..dcd8cb77 100644 --- a/internal/storage/hardcoded_test.go +++ b/internal/storage/hardcoded_test.go @@ -88,6 +88,11 @@ func Test_versions(t *testing.T) { version: allServers.Nordvpn.Version, digest: "a8043704", }, + "Perfect privacy": { + model: models.PerfectprivacyServer{}, + version: allServers.Perfectprivacy.Version, + digest: "233f0dd4", + }, "Privado": { model: models.PrivadoServer{}, version: allServers.Privado.Version, diff --git a/internal/storage/merge.go b/internal/storage/merge.go index 24e1d7ef..7b38dda4 100644 --- a/internal/storage/merge.go +++ b/internal/storage/merge.go @@ -28,26 +28,27 @@ func (s *Storage) logTimeDiff(provider string, persistedUnix, hardcodedUnix int6 func (s *Storage) mergeServers(hardcoded, persisted models.AllServers) models.AllServers { return models.AllServers{ - Version: hardcoded.Version, - Cyberghost: s.mergeCyberghost(hardcoded.Cyberghost, persisted.Cyberghost), - Expressvpn: s.mergeExpressvpn(hardcoded.Expressvpn, persisted.Expressvpn), - Fastestvpn: s.mergeFastestvpn(hardcoded.Fastestvpn, persisted.Fastestvpn), - HideMyAss: s.mergeHideMyAss(hardcoded.HideMyAss, persisted.HideMyAss), - Ipvanish: s.mergeIpvanish(hardcoded.Ipvanish, persisted.Ipvanish), - Ivpn: s.mergeIvpn(hardcoded.Ivpn, persisted.Ivpn), - Mullvad: s.mergeMullvad(hardcoded.Mullvad, persisted.Mullvad), - 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), - Protonvpn: s.mergeProtonvpn(hardcoded.Protonvpn, persisted.Protonvpn), - Purevpn: s.mergePureVPN(hardcoded.Purevpn, persisted.Purevpn), - Surfshark: s.mergeSurfshark(hardcoded.Surfshark, persisted.Surfshark), - Torguard: s.mergeTorguard(hardcoded.Torguard, persisted.Torguard), - VPNUnlimited: s.mergeVPNUnlimited(hardcoded.VPNUnlimited, persisted.VPNUnlimited), - Vyprvpn: s.mergeVyprvpn(hardcoded.Vyprvpn, persisted.Vyprvpn), - Wevpn: s.mergeWevpn(hardcoded.Wevpn, persisted.Wevpn), - Windscribe: s.mergeWindscribe(hardcoded.Windscribe, persisted.Windscribe), + Version: hardcoded.Version, + Cyberghost: s.mergeCyberghost(hardcoded.Cyberghost, persisted.Cyberghost), + Expressvpn: s.mergeExpressvpn(hardcoded.Expressvpn, persisted.Expressvpn), + Fastestvpn: s.mergeFastestvpn(hardcoded.Fastestvpn, persisted.Fastestvpn), + HideMyAss: s.mergeHideMyAss(hardcoded.HideMyAss, persisted.HideMyAss), + Ipvanish: s.mergeIpvanish(hardcoded.Ipvanish, persisted.Ipvanish), + Ivpn: s.mergeIvpn(hardcoded.Ivpn, persisted.Ivpn), + Mullvad: s.mergeMullvad(hardcoded.Mullvad, persisted.Mullvad), + Nordvpn: s.mergeNordVPN(hardcoded.Nordvpn, persisted.Nordvpn), + Perfectprivacy: s.mergePerfectprivacy(hardcoded.Perfectprivacy, persisted.Perfectprivacy), + Privado: s.mergePrivado(hardcoded.Privado, persisted.Privado), + Pia: s.mergePIA(hardcoded.Pia, persisted.Pia), + Privatevpn: s.mergePrivatevpn(hardcoded.Privatevpn, persisted.Privatevpn), + Protonvpn: s.mergeProtonvpn(hardcoded.Protonvpn, persisted.Protonvpn), + Purevpn: s.mergePureVPN(hardcoded.Purevpn, persisted.Purevpn), + Surfshark: s.mergeSurfshark(hardcoded.Surfshark, persisted.Surfshark), + Torguard: s.mergeTorguard(hardcoded.Torguard, persisted.Torguard), + VPNUnlimited: s.mergeVPNUnlimited(hardcoded.VPNUnlimited, persisted.VPNUnlimited), + Vyprvpn: s.mergeVyprvpn(hardcoded.Vyprvpn, persisted.Vyprvpn), + Wevpn: s.mergeWevpn(hardcoded.Wevpn, persisted.Wevpn), + Windscribe: s.mergeWindscribe(hardcoded.Windscribe, persisted.Windscribe), } } @@ -123,6 +124,15 @@ func (s *Storage) mergeNordVPN(hardcoded, persisted models.NordvpnServers) model return persisted } +func (s *Storage) mergePerfectprivacy(hardcoded, persisted models.PerfectprivacyServers) models.PerfectprivacyServers { + if persisted.Timestamp <= hardcoded.Timestamp { + return hardcoded + } + + s.logTimeDiff("Perfect Privacy", persisted.Timestamp, hardcoded.Timestamp) + return persisted +} + func (s *Storage) mergePrivado(hardcoded, persisted models.PrivadoServers) models.PrivadoServers { if persisted.Timestamp <= hardcoded.Timestamp { return hardcoded diff --git a/internal/storage/read.go b/internal/storage/read.go index f18b6f3e..2ed8038f 100644 --- a/internal/storage/read.go +++ b/internal/storage/read.go @@ -126,6 +126,15 @@ func (s *Storage) extractServersFromBytes(b []byte, hardcoded models.AllServers) } } + if hardcoded.Perfectprivacy.Version != versions.Perfectprivacy.Version { + s.logVersionDiff("Perfect Privacy", hardcoded.Perfectprivacy.Version, versions.Perfectprivacy.Version) + } else { + err = json.Unmarshal(rawMessages.Perfectprivacy, &servers.Perfectprivacy) + if err != nil { + return servers, fmt.Errorf("%w: %s: %s", errDecodeProvider, "Perfect Privacy", err) + } + } + if hardcoded.Privado.Version != versions.Privado.Version { s.logVersionDiff("Privado", hardcoded.Privado.Version, versions.Privado.Version) } else { @@ -231,26 +240,27 @@ func (s *Storage) extractServersFromBytes(b []byte, hardcoded models.AllServers) // allVersions is a subset of models.AllServers structure used to track // versions to avoid unmarshaling errors. type allVersions struct { - Version uint16 `json:"version"` // used for migration of the top level scheme - Cyberghost serverVersion `json:"cyberghost"` - Expressvpn serverVersion `json:"expressvpn"` - Fastestvpn serverVersion `json:"fastestvpn"` - HideMyAss serverVersion `json:"hidemyass"` - Ipvanish serverVersion `json:"ipvanish"` - Ivpn serverVersion `json:"ivpn"` - Mullvad serverVersion `json:"mullvad"` - Nordvpn serverVersion `json:"nordvpn"` - Privado serverVersion `json:"privado"` - Pia serverVersion `json:"pia"` - Privatevpn serverVersion `json:"privatevpn"` - Protonvpn serverVersion `json:"protonvpn"` - Purevpn serverVersion `json:"purevpn"` - Surfshark serverVersion `json:"surfshark"` - Torguard serverVersion `json:"torguard"` - VPNUnlimited serverVersion `json:"vpnunlimited"` - Vyprvpn serverVersion `json:"vyprvpn"` - Wevpn serverVersion `json:"wevpn"` - Windscribe serverVersion `json:"windscribe"` + Version uint16 `json:"version"` // used for migration of the top level scheme + Cyberghost serverVersion `json:"cyberghost"` + Expressvpn serverVersion `json:"expressvpn"` + Fastestvpn serverVersion `json:"fastestvpn"` + HideMyAss serverVersion `json:"hidemyass"` + Ipvanish serverVersion `json:"ipvanish"` + Ivpn serverVersion `json:"ivpn"` + Mullvad serverVersion `json:"mullvad"` + Nordvpn serverVersion `json:"nordvpn"` + Perfectprivacy serverVersion `json:"perfectprivacy"` + Privado serverVersion `json:"privado"` + Pia serverVersion `json:"pia"` + Privatevpn serverVersion `json:"privatevpn"` + Protonvpn serverVersion `json:"protonvpn"` + Purevpn serverVersion `json:"purevpn"` + Surfshark serverVersion `json:"surfshark"` + Torguard serverVersion `json:"torguard"` + VPNUnlimited serverVersion `json:"vpnunlimited"` + Vyprvpn serverVersion `json:"vyprvpn"` + Wevpn serverVersion `json:"wevpn"` + Windscribe serverVersion `json:"windscribe"` } type serverVersion struct { @@ -259,24 +269,25 @@ type serverVersion struct { // allJSONRawMessages is to delay decoding of each provider servers. type allJSONRawMessages struct { - Version uint16 `json:"version"` // used for migration of the top level scheme - Cyberghost json.RawMessage `json:"cyberghost"` - Expressvpn json.RawMessage `json:"expressvpn"` - Fastestvpn json.RawMessage `json:"fastestvpn"` - HideMyAss json.RawMessage `json:"hidemyass"` - Ipvanish json.RawMessage `json:"ipvanish"` - Ivpn json.RawMessage `json:"ivpn"` - Mullvad json.RawMessage `json:"mullvad"` - Nordvpn json.RawMessage `json:"nordvpn"` - Privado json.RawMessage `json:"privado"` - Pia json.RawMessage `json:"pia"` - Privatevpn json.RawMessage `json:"privatevpn"` - Protonvpn json.RawMessage `json:"protonvpn"` - Purevpn json.RawMessage `json:"purevpn"` - Surfshark json.RawMessage `json:"surfshark"` - Torguard json.RawMessage `json:"torguard"` - VPNUnlimited json.RawMessage `json:"vpnunlimited"` - Vyprvpn json.RawMessage `json:"vyprvpn"` - Wevpn json.RawMessage `json:"wevpn"` - Windscribe json.RawMessage `json:"windscribe"` + Version uint16 `json:"version"` // used for migration of the top level scheme + Cyberghost json.RawMessage `json:"cyberghost"` + Expressvpn json.RawMessage `json:"expressvpn"` + Fastestvpn json.RawMessage `json:"fastestvpn"` + HideMyAss json.RawMessage `json:"hidemyass"` + Ipvanish json.RawMessage `json:"ipvanish"` + Ivpn json.RawMessage `json:"ivpn"` + Mullvad json.RawMessage `json:"mullvad"` + Nordvpn json.RawMessage `json:"nordvpn"` + Perfectprivacy json.RawMessage `json:"perfectprivacy"` + Privado json.RawMessage `json:"privado"` + Pia json.RawMessage `json:"pia"` + Privatevpn json.RawMessage `json:"privatevpn"` + Protonvpn json.RawMessage `json:"protonvpn"` + Purevpn json.RawMessage `json:"purevpn"` + Surfshark json.RawMessage `json:"surfshark"` + Torguard json.RawMessage `json:"torguard"` + VPNUnlimited json.RawMessage `json:"vpnunlimited"` + Vyprvpn json.RawMessage `json:"vyprvpn"` + Wevpn json.RawMessage `json:"wevpn"` + Windscribe json.RawMessage `json:"windscribe"` } diff --git a/internal/storage/read_test.go b/internal/storage/read_test.go index c4ace4ad..c03b5a52 100644 --- a/internal/storage/read_test.go +++ b/internal/storage/read_test.go @@ -30,25 +30,26 @@ func Test_extractServersFromBytes(t *testing.T) { "different versions": { b: []byte(`{}`), hardcoded: models.AllServers{ - Cyberghost: models.CyberghostServers{Version: 1}, - Expressvpn: models.ExpressvpnServers{Version: 1}, - Fastestvpn: models.FastestvpnServers{Version: 1}, - HideMyAss: models.HideMyAssServers{Version: 1}, - Ipvanish: models.IpvanishServers{Version: 1}, - Ivpn: models.IvpnServers{Version: 1}, - Mullvad: models.MullvadServers{Version: 1}, - Nordvpn: models.NordvpnServers{Version: 1}, - Privado: models.PrivadoServers{Version: 1}, - Pia: models.PiaServers{Version: 1}, - Privatevpn: models.PrivatevpnServers{Version: 1}, - Protonvpn: models.ProtonvpnServers{Version: 1}, - Purevpn: models.PurevpnServers{Version: 1}, - Surfshark: models.SurfsharkServers{Version: 1}, - Torguard: models.TorguardServers{Version: 1}, - VPNUnlimited: models.VPNUnlimitedServers{Version: 1}, - Vyprvpn: models.VyprvpnServers{Version: 1}, - Wevpn: models.WevpnServers{Version: 1}, - Windscribe: models.WindscribeServers{Version: 1}, + Cyberghost: models.CyberghostServers{Version: 1}, + Expressvpn: models.ExpressvpnServers{Version: 1}, + Fastestvpn: models.FastestvpnServers{Version: 1}, + HideMyAss: models.HideMyAssServers{Version: 1}, + Ipvanish: models.IpvanishServers{Version: 1}, + Ivpn: models.IvpnServers{Version: 1}, + Mullvad: models.MullvadServers{Version: 1}, + Nordvpn: models.NordvpnServers{Version: 1}, + Perfectprivacy: models.PerfectprivacyServers{Version: 1}, + Privado: models.PrivadoServers{Version: 1}, + Pia: models.PiaServers{Version: 1}, + Privatevpn: models.PrivatevpnServers{Version: 1}, + Protonvpn: models.ProtonvpnServers{Version: 1}, + Purevpn: models.PurevpnServers{Version: 1}, + Surfshark: models.SurfsharkServers{Version: 1}, + Torguard: models.TorguardServers{Version: 1}, + VPNUnlimited: models.VPNUnlimitedServers{Version: 1}, + Vyprvpn: models.VyprvpnServers{Version: 1}, + Wevpn: models.WevpnServers{Version: 1}, + Windscribe: models.WindscribeServers{Version: 1}, }, logged: []string{ "Cyberghost servers from file discarded because they have version 0 and hardcoded servers have version 1", @@ -59,6 +60,7 @@ func Test_extractServersFromBytes(t *testing.T) { "Ivpn servers from file discarded because they have version 0 and hardcoded servers have version 1", "Mullvad servers from file discarded because they have version 0 and hardcoded servers have version 1", "Nordvpn servers from file discarded because they have version 0 and hardcoded servers have version 1", + "Perfect Privacy servers from file discarded because they have version 0 and hardcoded servers have version 1", "Privado servers from file discarded because they have version 0 and hardcoded servers have version 1", "Pia servers from file discarded because they have version 0 and hardcoded servers have version 1", "Privatevpn servers from file discarded because they have version 0 and hardcoded servers have version 1", @@ -82,6 +84,7 @@ func Test_extractServersFromBytes(t *testing.T) { "ivpn": {"version": 1, "timestamp": 1}, "mullvad": {"version": 1, "timestamp": 1}, "nordvpn": {"version": 1, "timestamp": 1}, + "perfectprivacy": {"version": 1, "timestamp": 1}, "privado": {"version": 1, "timestamp": 1}, "pia": {"version": 1, "timestamp": 1}, "privatevpn": {"version": 1, "timestamp": 1}, @@ -95,46 +98,48 @@ func Test_extractServersFromBytes(t *testing.T) { "windscribe": {"version": 1, "timestamp": 1} }`), hardcoded: models.AllServers{ - Cyberghost: models.CyberghostServers{Version: 1}, - Expressvpn: models.ExpressvpnServers{Version: 1}, - Fastestvpn: models.FastestvpnServers{Version: 1}, - HideMyAss: models.HideMyAssServers{Version: 1}, - Ipvanish: models.IpvanishServers{Version: 1}, - Ivpn: models.IvpnServers{Version: 1}, - Mullvad: models.MullvadServers{Version: 1}, - Nordvpn: models.NordvpnServers{Version: 1}, - Privado: models.PrivadoServers{Version: 1}, - Pia: models.PiaServers{Version: 1}, - Privatevpn: models.PrivatevpnServers{Version: 1}, - Protonvpn: models.ProtonvpnServers{Version: 1}, - Purevpn: models.PurevpnServers{Version: 1}, - Surfshark: models.SurfsharkServers{Version: 1}, - Torguard: models.TorguardServers{Version: 1}, - VPNUnlimited: models.VPNUnlimitedServers{Version: 1}, - Vyprvpn: models.VyprvpnServers{Version: 1}, - Wevpn: models.WevpnServers{Version: 1}, - Windscribe: models.WindscribeServers{Version: 1}, + Cyberghost: models.CyberghostServers{Version: 1}, + Expressvpn: models.ExpressvpnServers{Version: 1}, + Fastestvpn: models.FastestvpnServers{Version: 1}, + HideMyAss: models.HideMyAssServers{Version: 1}, + Ipvanish: models.IpvanishServers{Version: 1}, + Ivpn: models.IvpnServers{Version: 1}, + Mullvad: models.MullvadServers{Version: 1}, + Nordvpn: models.NordvpnServers{Version: 1}, + Perfectprivacy: models.PerfectprivacyServers{Version: 1}, + Privado: models.PrivadoServers{Version: 1}, + Pia: models.PiaServers{Version: 1}, + Privatevpn: models.PrivatevpnServers{Version: 1}, + Protonvpn: models.ProtonvpnServers{Version: 1}, + Purevpn: models.PurevpnServers{Version: 1}, + Surfshark: models.SurfsharkServers{Version: 1}, + Torguard: models.TorguardServers{Version: 1}, + VPNUnlimited: models.VPNUnlimitedServers{Version: 1}, + Vyprvpn: models.VyprvpnServers{Version: 1}, + Wevpn: models.WevpnServers{Version: 1}, + Windscribe: models.WindscribeServers{Version: 1}, }, persisted: models.AllServers{ - Cyberghost: models.CyberghostServers{Version: 1, Timestamp: 1}, - Expressvpn: models.ExpressvpnServers{Version: 1, Timestamp: 1}, - Fastestvpn: models.FastestvpnServers{Version: 1, Timestamp: 1}, - HideMyAss: models.HideMyAssServers{Version: 1, Timestamp: 1}, - Ipvanish: models.IpvanishServers{Version: 1, Timestamp: 1}, - Ivpn: models.IvpnServers{Version: 1, Timestamp: 1}, - Mullvad: models.MullvadServers{Version: 1, Timestamp: 1}, - Nordvpn: models.NordvpnServers{Version: 1, Timestamp: 1}, - Privado: models.PrivadoServers{Version: 1, Timestamp: 1}, - Pia: models.PiaServers{Version: 1, Timestamp: 1}, - Privatevpn: models.PrivatevpnServers{Version: 1, Timestamp: 1}, - Protonvpn: models.ProtonvpnServers{Version: 1, Timestamp: 1}, - Purevpn: models.PurevpnServers{Version: 1, Timestamp: 1}, - Surfshark: models.SurfsharkServers{Version: 1, Timestamp: 1}, - Torguard: models.TorguardServers{Version: 1, Timestamp: 1}, - VPNUnlimited: models.VPNUnlimitedServers{Version: 1, Timestamp: 1}, - Vyprvpn: models.VyprvpnServers{Version: 1, Timestamp: 1}, - Wevpn: models.WevpnServers{Version: 1, Timestamp: 1}, - Windscribe: models.WindscribeServers{Version: 1, Timestamp: 1}, + Cyberghost: models.CyberghostServers{Version: 1, Timestamp: 1}, + Expressvpn: models.ExpressvpnServers{Version: 1, Timestamp: 1}, + Fastestvpn: models.FastestvpnServers{Version: 1, Timestamp: 1}, + HideMyAss: models.HideMyAssServers{Version: 1, Timestamp: 1}, + Ipvanish: models.IpvanishServers{Version: 1, Timestamp: 1}, + Ivpn: models.IvpnServers{Version: 1, Timestamp: 1}, + Mullvad: models.MullvadServers{Version: 1, Timestamp: 1}, + Nordvpn: models.NordvpnServers{Version: 1, Timestamp: 1}, + Perfectprivacy: models.PerfectprivacyServers{Version: 1, Timestamp: 1}, + Privado: models.PrivadoServers{Version: 1, Timestamp: 1}, + Pia: models.PiaServers{Version: 1, Timestamp: 1}, + Privatevpn: models.PrivatevpnServers{Version: 1, Timestamp: 1}, + Protonvpn: models.ProtonvpnServers{Version: 1, Timestamp: 1}, + Purevpn: models.PurevpnServers{Version: 1, Timestamp: 1}, + Surfshark: models.SurfsharkServers{Version: 1, Timestamp: 1}, + Torguard: models.TorguardServers{Version: 1, Timestamp: 1}, + VPNUnlimited: models.VPNUnlimitedServers{Version: 1, Timestamp: 1}, + Vyprvpn: models.VyprvpnServers{Version: 1, Timestamp: 1}, + Wevpn: models.WevpnServers{Version: 1, Timestamp: 1}, + Windscribe: models.WindscribeServers{Version: 1, Timestamp: 1}, }, }, } diff --git a/internal/storage/servers.json b/internal/storage/servers.json index 908e9b7c..d9775b90 100644 --- a/internal/storage/servers.json +++ b/internal/storage/servers.json @@ -39835,6 +39835,421 @@ } ] }, + "perfectprivacy": { + "version": 1, + "timestamp": 1633357036, + "servers": [ + { + "city": "Amsterdam", + "ips": [ + "95.211.95.244", + "95.211.95.244", + "95.211.95.244", + "37.48.94.1", + "85.17.64.131", + "95.211.95.232", + "85.17.28.145" + ], + "tcp": true, + "udp": true + }, + { + "city": "Basel", + "ips": [ + "82.199.134.162", + "82.199.134.162", + "82.199.134.162", + "80.255.7.66" + ], + "tcp": true, + "udp": true + }, + { + "city": "Belgrade", + "ips": [ + "152.89.160.98", + "152.89.160.98", + "152.89.160.98" + ], + "tcp": true, + "udp": true + }, + { + "city": "Berlin", + "ips": [ + "80.255.7.98", + "80.255.7.98", + "80.255.7.98" + ], + "tcp": true, + "udp": true + }, + { + "city": "Bucharest", + "ips": [ + "185.57.82.25", + "185.57.82.25", + "185.57.82.25" + ], + "tcp": true, + "udp": true + }, + { + "city": "Calais", + "ips": [ + "149.202.77.77", + "149.202.77.77", + "149.202.77.77" + ], + "tcp": true, + "udp": true + }, + { + "city": "Chicago", + "ips": [ + "104.237.193.26", + "104.237.193.26", + "104.237.193.26" + ], + "tcp": true, + "udp": true + }, + { + "city": "Copenhagen", + "ips": [ + "185.152.32.66", + "185.152.32.66", + "185.152.32.66" + ], + "tcp": true, + "udp": true + }, + { + "city": "Dallas", + "ips": [ + "138.128.136.164", + "138.128.136.164", + "138.128.136.164" + ], + "tcp": true, + "udp": true + }, + { + "city": "Erfurt", + "ips": [ + "217.114.218.18", + "217.114.218.18", + "217.114.218.18" + ], + "tcp": true, + "udp": true + }, + { + "city": "Frankfurt", + "ips": [ + "178.162.194.30", + "178.162.194.30", + "178.162.194.30", + "37.58.58.239" + ], + "tcp": true, + "udp": true + }, + { + "city": "Hamburg", + "ips": [ + "80.255.7.114", + "80.255.7.114", + "80.255.7.114" + ], + "tcp": true, + "udp": true + }, + { + "city": "Hongkong", + "ips": [ + "209.58.188.129", + "209.58.188.129", + "209.58.188.129" + ], + "tcp": true, + "udp": true + }, + { + "city": "Istanbul", + "ips": [ + "185.65.205.18", + "185.65.205.18", + "185.65.205.18" + ], + "tcp": true, + "udp": true + }, + { + "city": "Jerusalem", + "ips": [ + "82.81.85.231", + "82.81.85.231", + "82.81.85.231" + ], + "tcp": true, + "udp": true + }, + { + "city": "London", + "ips": [ + "82.199.130.34", + "82.199.130.34", + "82.199.130.34", + "5.187.21.98" + ], + "tcp": true, + "udp": true + }, + { + "city": "LosAngeles", + "ips": [ + "162.245.206.242", + "162.245.206.242", + "162.245.206.242" + ], + "tcp": true, + "udp": true + }, + { + "city": "Madrid", + "ips": [ + "185.183.106.146", + "185.183.106.146", + "185.183.106.146" + ], + "tcp": true, + "udp": true + }, + { + "city": "Malmoe", + "ips": [ + "194.68.170.51", + "194.68.170.51", + "194.68.170.51" + ], + "tcp": true, + "udp": true + }, + { + "city": "Manchester", + "ips": [ + "217.138.196.98", + "217.138.196.98", + "217.138.196.98" + ], + "tcp": true, + "udp": true + }, + { + "city": "Miami", + "ips": [ + "38.132.118.66", + "38.132.118.66", + "38.132.118.66" + ], + "tcp": true, + "udp": true + }, + { + "city": "Milan", + "ips": [ + "192.145.127.210", + "192.145.127.210", + "192.145.127.210" + ], + "tcp": true, + "udp": true + }, + { + "city": "Montreal", + "ips": [ + "167.114.209.103", + "167.114.209.103", + "167.114.209.103" + ], + "tcp": true, + "udp": true + }, + { + "city": "Moscow", + "ips": [ + "192.162.100.241", + "192.162.100.241", + "192.162.100.241", + "192.162.100.240" + ], + "tcp": true, + "udp": true + }, + { + "city": "NewYork", + "ips": [ + "96.9.246.194", + "96.9.246.194", + "96.9.246.194" + ], + "tcp": true, + "udp": true + }, + { + "city": "Nuremberg", + "ips": [ + "81.95.5.34", + "81.95.5.34", + "81.95.5.34", + "80.255.10.194" + ], + "tcp": true, + "udp": true + }, + { + "city": "Oslo", + "ips": [ + "91.205.187.186", + "91.205.187.186", + "91.205.187.186" + ], + "tcp": true, + "udp": true + }, + { + "city": "Paris", + "ips": [ + "5.135.143.84", + "5.135.143.84", + "5.135.143.84" + ], + "tcp": true, + "udp": true + }, + { + "city": "Prague", + "ips": [ + "195.138.249.2", + "195.138.249.2", + "195.138.249.2" + ], + "tcp": true, + "udp": true + }, + { + "city": "Reykjavik", + "ips": [ + "82.221.105.61", + "82.221.105.61", + "82.221.105.61" + ], + "tcp": true, + "udp": true + }, + { + "city": "Riga", + "ips": [ + "46.183.221.194", + "46.183.221.194", + "46.183.221.194" + ], + "tcp": true, + "udp": true + }, + { + "city": "Rotterdam", + "ips": [ + "31.204.152.102", + "31.204.152.102", + "31.204.152.102", + "31.204.152.189", + "31.204.150.106", + "31.204.150.138", + "31.204.153.106" + ], + "tcp": true, + "udp": true + }, + { + "city": "Singapore", + "ips": [ + "103.254.153.202", + "103.254.153.202", + "103.254.153.202", + "209.58.162.197" + ], + "tcp": true, + "udp": true + }, + { + "city": "Steinsel", + "ips": [ + "94.242.243.162", + "94.242.243.162", + "94.242.243.162", + "94.242.243.66" + ], + "tcp": true, + "udp": true + }, + { + "city": "Stockholm", + "ips": [ + "185.41.240.18", + "185.41.240.18", + "185.41.240.18", + "185.217.1.2" + ], + "tcp": true, + "udp": true + }, + { + "city": "Strasbourg", + "ips": [ + "37.187.163.66", + "37.187.163.66", + "37.187.163.66" + ], + "tcp": true, + "udp": true + }, + { + "city": "Sydney", + "ips": [ + "66.203.112.50", + "66.203.112.50", + "66.203.112.50", + "66.203.112.47" + ], + "tcp": true, + "udp": true + }, + { + "city": "Tokyo", + "ips": [ + "31.204.145.166", + "31.204.145.166", + "31.204.145.166" + ], + "tcp": true, + "udp": true + }, + { + "city": "Zurich", + "ips": [ + "37.120.213.210", + "37.120.213.210", + "37.120.213.210", + "37.120.213.194", + "152.89.162.226" + ], + "tcp": true, + "udp": true + } + ] + }, "nordvpn": { "version": 3, "timestamp": 1627008323, @@ -111526,4 +111941,4 @@ } ] } -} \ No newline at end of file +} diff --git a/internal/storage/sync.go b/internal/storage/sync.go index 584b329b..2466121d 100644 --- a/internal/storage/sync.go +++ b/internal/storage/sync.go @@ -22,6 +22,7 @@ func countServers(allServers models.AllServers) int { len(allServers.Ivpn.Servers) + len(allServers.Mullvad.Servers) + len(allServers.Nordvpn.Servers) + + len(allServers.Perfectprivacy.Servers) + len(allServers.Privado.Servers) + len(allServers.Pia.Servers) + len(allServers.Privatevpn.Servers) + diff --git a/internal/updater/openvpn/extract.go b/internal/updater/openvpn/extract.go index d5a225d4..c07f3fd4 100644 --- a/internal/updater/openvpn/extract.go +++ b/internal/updater/openvpn/extract.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "net" + "sort" "strings" ) @@ -53,20 +54,38 @@ func ExtractHost(b []byte) (host, warning string, err error) { return hosts[0], warning, nil } +func ExtractIPs(b []byte) (ips []net.IP, err error) { + const rejectIP, rejectDomain = false, true + ipStrings := extractRemoteHosts(b, rejectIP, rejectDomain) + if len(ipStrings) == 0 { + return nil, ErrNoRemoteIP + } + + sort.Slice(ipStrings, func(i, j int) bool { + return ipStrings[i] < ipStrings[j] + }) + + ips = make([]net.IP, len(ipStrings)) + for i := range ipStrings { + ips[i] = net.ParseIP(ipStrings[i]) + } + + return ips, nil +} + func ExtractIP(b []byte) (ip net.IP, warning string, err error) { - const ( - rejectIP = false - rejectDomain = true - ) - ips := extractRemoteHosts(b, rejectIP, rejectDomain) - if len(ips) == 0 { - return nil, "", ErrNoRemoteIP - } else if len(ips) > 1 { + ips, err := ExtractIPs(b) + if err != nil { + return nil, "", err + } + + if len(ips) > 1 { warning = fmt.Sprintf( "only using the first IP address %s and discarding %d other hosts", ips[0], len(ips)-1) } - return net.ParseIP(ips[0]), warning, nil + + return ips[0], warning, nil } func extractRemoteHosts(content []byte, rejectIP, rejectDomain bool) (hosts []string) { diff --git a/internal/updater/providers.go b/internal/updater/providers.go index b2b25eea..261539ac 100644 --- a/internal/updater/providers.go +++ b/internal/updater/providers.go @@ -14,6 +14,7 @@ import ( "github.com/qdm12/gluetun/internal/updater/providers/ivpn" "github.com/qdm12/gluetun/internal/updater/providers/mullvad" "github.com/qdm12/gluetun/internal/updater/providers/nordvpn" + "github.com/qdm12/gluetun/internal/updater/providers/perfectprivacy" "github.com/qdm12/gluetun/internal/updater/providers/pia" "github.com/qdm12/gluetun/internal/updater/providers/privado" "github.com/qdm12/gluetun/internal/updater/providers/privatevpn" @@ -190,6 +191,27 @@ func (u *updater) updateNordvpn(ctx context.Context) (err error) { return nil } +func (u *updater) updatePerfectprivacy(ctx context.Context) (err error) { + minServers := getMinServers(len(u.servers.Perfectprivacy.Servers)) + servers, warnings, err := perfectprivacy.GetServers(ctx, u.unzipper, minServers) + if u.options.CLI { + for _, warning := range warnings { + u.logger.Warn(constants.Perfectprivacy + ": " + warning) + } + } + if err != nil { + return err + } + + if reflect.DeepEqual(u.servers.Perfectprivacy.Servers, servers) { + return nil + } + + u.servers.Perfectprivacy.Timestamp = u.timeNow().Unix() + u.servers.Perfectprivacy.Servers = servers + return nil +} + func (u *updater) updatePIA(ctx context.Context) (err error) { minServers := getMinServers(len(u.servers.Pia.Servers)) servers, err := pia.GetServers(ctx, u.client, minServers) diff --git a/internal/updater/providers/perfectprivacy/citytoserver.go b/internal/updater/providers/perfectprivacy/citytoserver.go new file mode 100644 index 00000000..b1c4d3a3 --- /dev/null +++ b/internal/updater/providers/perfectprivacy/citytoserver.go @@ -0,0 +1,45 @@ +package perfectprivacy + +import ( + "net" + + "github.com/qdm12/gluetun/internal/models" +) + +type cityToServer map[string]models.PerfectprivacyServer + +func (cts cityToServer) add(city string, ips []net.IP) { + server, ok := cts[city] + if !ok { + server.City = city + server.IPs = ips + server.TCP = true + server.UDP = true + } else { + // Do not insert duplicate IP addresses + existingIPs := make(map[string]struct{}, len(server.IPs)) + for _, ip := range server.IPs { + existingIPs[ip.String()] = struct{}{} + } + + for _, ip := range ips { + ipString := ip.String() + _, ok := existingIPs[ipString] + if ok { + continue + } + existingIPs[ipString] = struct{}{} + server.IPs = append(server.IPs, ip) + } + } + + cts[city] = server +} + +func (cts cityToServer) toServersSlice() (servers []models.PerfectprivacyServer) { + servers = make([]models.PerfectprivacyServer, 0, len(cts)) + for _, server := range cts { + servers = append(servers, server) + } + return servers +} diff --git a/internal/updater/providers/perfectprivacy/filename.go b/internal/updater/providers/perfectprivacy/filename.go new file mode 100644 index 00000000..7378ac9d --- /dev/null +++ b/internal/updater/providers/perfectprivacy/filename.go @@ -0,0 +1,20 @@ +package perfectprivacy + +import ( + "strings" + "unicode" +) + +func parseFilename(fileName string) (city string) { + const suffix = ".conf" + s := strings.TrimSuffix(fileName, suffix) + + for i, r := range s { + if unicode.IsDigit(r) { + s = s[:i] + break + } + } + + return s +} diff --git a/internal/updater/providers/perfectprivacy/servers.go b/internal/updater/providers/perfectprivacy/servers.go new file mode 100644 index 00000000..1e0746c0 --- /dev/null +++ b/internal/updater/providers/perfectprivacy/servers.go @@ -0,0 +1,73 @@ +package perfectprivacy + +import ( + "context" + "errors" + "fmt" + "net/url" + "strings" + + "github.com/qdm12/gluetun/internal/models" + "github.com/qdm12/gluetun/internal/updater/openvpn" + "github.com/qdm12/gluetun/internal/updater/unzip" +) + +var ErrNotEnoughServers = errors.New("not enough servers found") + +func GetServers(ctx context.Context, unzipper unzip.Unzipper, minServers int) ( + servers []models.PerfectprivacyServer, warnings []string, err error) { + zipURL := url.URL{ + Scheme: "https", + Host: "www.perfect-privacy.com", + Path: "/downloads/openvpn/get", + } + values := make(url.Values) + values.Set("system", "linux") + values.Set("scope", "server") + values.Set("filetype", "zip") + values.Set("protocol", "udp") // all support both TCP and UDP + zipURL.RawQuery = values.Encode() + + contents, err := unzipper.FetchAndExtract(ctx, zipURL.String()) + if err != nil { + return nil, nil, err + } + + cts := make(cityToServer) + + for fileName, content := range contents { + err := addServerFromOvpn(cts, fileName, content) + if err != nil { + warnings = append(warnings, fileName+": "+err.Error()) + } + } + + if len(cts) < minServers { + return nil, warnings, fmt.Errorf("%w: %d and expected at least %d", + ErrNotEnoughServers, len(cts), minServers) + } + + servers = cts.toServersSlice() + + sortServers(servers) + + return servers, warnings, nil +} + +func addServerFromOvpn(cts cityToServer, + fileName string, content []byte) (err error) { + if !strings.HasSuffix(fileName, ".conf") { + return nil // not an OpenVPN file + } + + ips, err := openvpn.ExtractIPs(content) + if err != nil { + return err + } + + city := parseFilename(fileName) + + cts.add(city, ips) + + return nil +} diff --git a/internal/updater/providers/perfectprivacy/sort.go b/internal/updater/providers/perfectprivacy/sort.go new file mode 100644 index 00000000..b8f5a41e --- /dev/null +++ b/internal/updater/providers/perfectprivacy/sort.go @@ -0,0 +1,13 @@ +package perfectprivacy + +import ( + "sort" + + "github.com/qdm12/gluetun/internal/models" +) + +func sortServers(servers []models.PerfectprivacyServer) { + sort.Slice(servers, func(i, j int) bool { + return servers[i].City < servers[j].City + }) +} diff --git a/internal/updater/unzip/extract.go b/internal/updater/unzip/extract.go index f0e86a55..481dcdbb 100644 --- a/internal/updater/unzip/extract.go +++ b/internal/updater/unzip/extract.go @@ -16,7 +16,8 @@ func zipExtractAll(zipBytes []byte) (contents map[string][]byte, err error) { contents = map[string][]byte{} for _, zf := range r.File { fileName := filepath.Base(zf.Name) - if !strings.HasSuffix(fileName, ".ovpn") { + if !strings.HasSuffix(fileName, ".ovpn") && + !strings.HasSuffix(fileName, ".conf") { continue } f, err := zf.Open() diff --git a/internal/updater/unzip/fetch.go b/internal/updater/unzip/fetch.go index 12cc8cb8..87617fc1 100644 --- a/internal/updater/unzip/fetch.go +++ b/internal/updater/unzip/fetch.go @@ -14,8 +14,6 @@ var ( func (u *unzipper) FetchAndExtract(ctx context.Context, url string) ( contents map[string][]byte, err error) { - contents = make(map[string][]byte) - request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, err @@ -40,13 +38,5 @@ func (u *unzipper) FetchAndExtract(ctx context.Context, url string) ( return nil, err } - newContents, err := zipExtractAll(b) - if err != nil { - return nil, err - } - for fileName, content := range newContents { - contents[fileName] = content - } - - return contents, nil + return zipExtractAll(b) } diff --git a/internal/updater/updater.go b/internal/updater/updater.go index 54968a05..8664bc79 100644 --- a/internal/updater/updater.go +++ b/internal/updater/updater.go @@ -132,6 +132,16 @@ func (u *updater) UpdateServers(ctx context.Context) (allServers models.AllServe } } + if u.options.Perfectprivacy { + u.logger.Info("updating " + constants.Perfectprivacy + " servers...") + if err := u.updatePerfectprivacy(ctx); err != nil { + if ctxErr := ctx.Err(); ctxErr != nil { + return allServers, ctxErr + } + u.logger.Error(err.Error()) + } + } + if u.options.Privado { u.logger.Info("updating Privado servers...") if err := u.updatePrivado(ctx); err != nil {