From d8e008606f67bde2ef6df09decfd6354afd5b8df Mon Sep 17 00:00:00 2001 From: Quentin McGaw Date: Thu, 23 Sep 2021 07:58:13 -0700 Subject: [PATCH] Feat: WeVPN support (#591) --- .github/labels.yml | 3 + README.md | 4 +- internal/cli/formatservers.go | 5 +- internal/cli/update.go | 1 + internal/configuration/openvpn.go | 2 + internal/configuration/provider.go | 5 +- internal/configuration/provider_test.go | 21 + internal/configuration/selection.go | 6 +- internal/configuration/updater.go | 2 + internal/configuration/wevpn.go | 57 ++ internal/constants/vpn.go | 2 + internal/constants/wevpn.go | 26 + internal/models/getservers.go | 12 + internal/models/markdown.go | 13 + internal/models/server.go | 8 + internal/models/servers.go | 7 + internal/provider/provider.go | 3 + internal/provider/wevpn/connection.go | 43 ++ internal/provider/wevpn/connection_test.go | 96 +++ internal/provider/wevpn/filter.go | 27 + internal/provider/wevpn/filter_test.go | 104 +++ internal/provider/wevpn/openvpnconf.go | 84 +++ internal/provider/wevpn/provider.go | 23 + internal/storage/hardcoded_test.go | 5 + internal/storage/merge.go | 16 + internal/storage/servers.json | 629 +++++++++++++++++- internal/storage/sync.go | 1 + internal/updater/providers.go | 22 + internal/updater/providers/wevpn/cities.go | 76 +++ internal/updater/providers/wevpn/hostname.go | 24 + internal/updater/providers/wevpn/resolve.go | 33 + .../updater/providers/wevpn/resolve_test.go | 57 ++ internal/updater/providers/wevpn/servers.go | 58 ++ internal/updater/providers/wevpn/sort.go | 16 + internal/updater/providers/wevpn/sort_test.go | 40 ++ internal/updater/updater.go | 10 + 36 files changed, 1533 insertions(+), 8 deletions(-) create mode 100644 internal/configuration/wevpn.go create mode 100644 internal/constants/wevpn.go create mode 100644 internal/provider/wevpn/connection.go create mode 100644 internal/provider/wevpn/connection_test.go create mode 100644 internal/provider/wevpn/filter.go create mode 100644 internal/provider/wevpn/filter_test.go create mode 100644 internal/provider/wevpn/openvpnconf.go create mode 100644 internal/provider/wevpn/provider.go create mode 100644 internal/updater/providers/wevpn/cities.go create mode 100644 internal/updater/providers/wevpn/hostname.go create mode 100644 internal/updater/providers/wevpn/resolve.go create mode 100644 internal/updater/providers/wevpn/resolve_test.go create mode 100644 internal/updater/providers/wevpn/servers.go create mode 100644 internal/updater/providers/wevpn/sort.go create mode 100644 internal/updater/providers/wevpn/sort_test.go diff --git a/.github/labels.yml b/.github/labels.yml index 0f9a8ce2..7e5c013b 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -62,6 +62,9 @@ - name: ":cloud: Vyprvpn" color: "cfe8d4" description: "" +- name: ":cloud: WeVPN" + color: "cfe8d4" + description: "" - name: ":cloud: Windscribe" color: "cfe8d4" description: "" diff --git a/README.md b/README.md index 12a74aaa..25ce57db 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ *Lightweight swiss-knife-like VPN client to tunnel to Cyberghost, FastestVPN, HideMyAss, IPVanish, IVPN, Mullvad, NordVPN, Privado, Private Internet Access, PrivateVPN, -ProtonVPN, PureVPN, Surfshark, TorGuard, VPNUnlimited, VyprVPN and Windscribe VPN servers +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* **ANNOUNCEMENT**: Wireguard is now supported for all providers supporting it! @@ -60,7 +60,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**, **FastestVPN**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **Surfshark**, **TorGuard**, **VPNUnlimited**, **Vyprvpn**, **Windscribe** servers +- Supports: **Cyberghost**, **FastestVPN**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **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 5d021f76..508ec1a9 100644 --- a/internal/cli/formatservers.go +++ b/internal/cli/formatservers.go @@ -27,7 +27,7 @@ func (c *CLI) FormatServers(args []string) error { var format, output string var cyberghost, fastestvpn, hideMyAss, ipvanish, ivpn, mullvad, nordvpn, pia, privado, privatevpn, protonvpn, purevpn, surfshark, - torguard, vpnUnlimited, vyprvpn, windscribe bool + torguard, vpnUnlimited, vyprvpn, wevpn, windscribe bool flagSet := flag.NewFlagSet("markdown", flag.ExitOnError) flagSet.StringVar(&format, "format", "markdown", "Format to use which can be: 'markdown'") flagSet.StringVar(&output, "output", "/dev/stdout", "Output file to write the formatted data to") @@ -47,6 +47,7 @@ func (c *CLI) FormatServers(args []string) error { flagSet.BoolVar(&torguard, "torguard", false, "Format Torguard servers") flagSet.BoolVar(&vpnUnlimited, "vpnunlimited", false, "Format VPN Unlimited servers") flagSet.BoolVar(&vyprvpn, "vyprvpn", false, "Format Vyprvpn servers") + flagSet.BoolVar(&wevpn, "wevpn", false, "Format WeVPN servers") flagSet.BoolVar(&windscribe, "windscribe", false, "Format Windscribe servers") if err := flagSet.Parse(args); err != nil { return err @@ -97,6 +98,8 @@ func (c *CLI) FormatServers(args []string) error { formatted = currentServers.VPNUnlimited.ToMarkdown() case vyprvpn: formatted = currentServers.Vyprvpn.ToMarkdown() + case wevpn: + formatted = currentServers.Wevpn.ToMarkdown() case windscribe: formatted = currentServers.Windscribe.ToMarkdown() default: diff --git a/internal/cli/update.go b/internal/cli/update.go index 1ebc915d..55354745 100644 --- a/internal/cli/update.go +++ b/internal/cli/update.go @@ -54,6 +54,7 @@ func (c *CLI) Update(ctx context.Context, args []string, logger logging.Logger) flagSet.BoolVar(&options.Torguard, "torguard", false, "Update Torguard servers") flagSet.BoolVar(&options.VPNUnlimited, "vpnunlimited", false, "Update VPN Unlimited servers") flagSet.BoolVar(&options.Vyprvpn, "vyprvpn", false, "Update Vyprvpn servers") + flagSet.BoolVar(&options.Wevpn, "wevpn", false, "Update WeVPN servers") flagSet.BoolVar(&options.Windscribe, "windscribe", false, "Update Windscribe servers") if err := flagSet.Parse(args); err != nil { return err diff --git a/internal/configuration/openvpn.go b/internal/configuration/openvpn.go index 2f5d99d1..69195d83 100644 --- a/internal/configuration/openvpn.go +++ b/internal/configuration/openvpn.go @@ -168,6 +168,8 @@ func (settings *OpenVPN) read(r reader, serviceProvider string) (err error) { settings.EncPreset, err = getPIAEncryptionPreset(r) case constants.VPNUnlimited: err = settings.readVPNUnlimited(r) + case constants.Wevpn: + err = settings.readWevpn(r) } if err != nil { return err diff --git a/internal/configuration/provider.go b/internal/configuration/provider.go index 70713706..f681d1f7 100644 --- a/internal/configuration/provider.go +++ b/internal/configuration/provider.go @@ -83,6 +83,8 @@ func (settings *Provider) read(r reader, vpnType string) error { err = settings.readVPNUnlimited(r) case constants.Vyprvpn: err = settings.readVyprvpn(r) + case constants.Wevpn: + err = settings.readWevpn(r) case constants.Windscribe: err = settings.readWindscribe(r) default: @@ -104,7 +106,8 @@ func (settings *Provider) readVPNServiceProvider(r reader, vpnType string) (err constants.Custom, "cyberghost", "fastestvpn", "hidemyass", "ipvanish", "ivpn", "mullvad", "nordvpn", "privado", "pia", "private internet access", "privatevpn", "protonvpn", - "purevpn", "surfshark", "torguard", constants.VPNUnlimited, "vyprvpn", "windscribe"} + "purevpn", "surfshark", "torguard", constants.VPNUnlimited, "vyprvpn", + constants.Wevpn, "windscribe"} case constants.Wireguard: allowedVPNServiceProviders = []string{ constants.Custom, constants.Ivpn, diff --git a/internal/configuration/provider_test.go b/internal/configuration/provider_test.go index 0cba4ab2..d301b360 100644 --- a/internal/configuration/provider_test.go +++ b/internal/configuration/provider_test.go @@ -322,6 +322,27 @@ func Test_Provider_lines(t *testing.T) { " |--Protocol: udp", }, }, + "wevpn": { + settings: Provider{ + Name: constants.Wevpn, + ServerSelection: ServerSelection{ + VPN: constants.OpenVPN, + Cities: []string{"a", "b"}, + Hostnames: []string{"c", "d"}, + OpenVPN: OpenVPNSelection{ + CustomPort: 1, + }, + }, + }, + lines: []string{ + "|--Wevpn settings:", + " |--Cities: a, b", + " |--Hostnames: c, d", + " |--OpenVPN selection:", + " |--Protocol: udp", + " |--Custom port: 1", + }, + }, "windscribe": { settings: Provider{ Name: constants.Windscribe, diff --git a/internal/configuration/selection.go b/internal/configuration/selection.go index 192c5fea..1db5c55d 100644 --- a/internal/configuration/selection.go +++ b/internal/configuration/selection.go @@ -17,9 +17,9 @@ type ServerSelection struct { //nolint:maligned // Fastestvpn, HideMyAss, IPVanish, IVPN, Mullvad, PrivateVPN, Protonvpn, PureVPN, VPNUnlimited Countries []string `json:"countries"` - // HideMyAss, IPVanish, IVPN, Mullvad, PrivateVPN, Protonvpn, PureVPN, VPNUnlimited, Windscribe + // HideMyAss, IPVanish, IVPN, Mullvad, PrivateVPN, Protonvpn, PureVPN, VPNUnlimited, WeVPN, Windscribe Cities []string `json:"cities"` - // Fastestvpn, HideMyAss, IPVanish, IVPN, PrivateVPN, Windscribe, Privado, Protonvpn, VPNUnlimited + // Fastestvpn, HideMyAss, IPVanish, IVPN, PrivateVPN, Windscribe, Privado, Protonvpn, VPNUnlimited, WeVPN Hostnames []string `json:"hostnames"` Names []string `json:"names"` // Protonvpn @@ -100,7 +100,7 @@ func (selection ServerSelection) toLines() (lines []string) { type OpenVPNSelection struct { ConfFile string `json:"conf_file"` // Custom configuration file path TCP bool `json:"tcp"` // UDP if TCP is false - CustomPort uint16 `json:"custom_port"` // HideMyAss, Mullvad, PIA, ProtonVPN, Windscribe + CustomPort uint16 `json:"custom_port"` // HideMyAss, Mullvad, PIA, ProtonVPN, WeVPN, Windscribe EncPreset string `json:"encryption_preset"` // PIA - needed to get the port number } diff --git a/internal/configuration/updater.go b/internal/configuration/updater.go index 9b389b73..de5867dc 100644 --- a/internal/configuration/updater.go +++ b/internal/configuration/updater.go @@ -27,6 +27,7 @@ type Updater struct { 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:"-"` @@ -65,6 +66,7 @@ func (settings *Updater) EnableAll() { settings.Torguard = true settings.VPNUnlimited = true settings.Vyprvpn = true + settings.Wevpn = true settings.Windscribe = true } diff --git a/internal/configuration/wevpn.go b/internal/configuration/wevpn.go new file mode 100644 index 00000000..fd6ccb25 --- /dev/null +++ b/internal/configuration/wevpn.go @@ -0,0 +1,57 @@ +package configuration + +import ( + "fmt" + + "github.com/qdm12/gluetun/internal/constants" +) + +func (settings *Provider) readWevpn(r reader) (err error) { + settings.Name = constants.Wevpn + servers := r.servers.GetWevpn() + + settings.ServerSelection.TargetIP, err = readTargetIP(r.env) + if err != nil { + return err + } + + settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.WevpnCityChoices(servers)) + if err != nil { + return fmt.Errorf("environment variable CITY: %w", err) + } + + settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.WevpnHostnameChoices(servers)) + if err != nil { + return fmt.Errorf("environment variable SERVER_HOSTNAME: %w", err) + } + + return settings.ServerSelection.OpenVPN.readWevpn(r) +} + +func (settings *OpenVPNSelection) readWevpn(r reader) (err error) { + settings.TCP, err = readOpenVPNProtocol(r) + if err != nil { + return err + } + + validation := openvpnPortValidation{ + tcp: settings.TCP, + allowedTCP: []uint16{53, 1195, 1199, 2018}, + allowedUDP: []uint16{80, 1194, 1198}, + } + settings.CustomPort, err = readOpenVPNCustomPort(r, validation) + if err != nil { + return err + } + + return nil +} + +func (settings *OpenVPN) readWevpn(r reader) (err error) { + settings.ClientKey, err = readClientKey(r) + if err != nil { + return err + } + + return nil +} diff --git a/internal/constants/vpn.go b/internal/constants/vpn.go index 80b245ef..4369d207 100644 --- a/internal/constants/vpn.go +++ b/internal/constants/vpn.go @@ -41,6 +41,8 @@ const ( VPNUnlimited = "vpn unlimited" // Vyprvpn is a VPN provider. Vyprvpn = "vyprvpn" + // WeVPN is a VPN provider. + Wevpn = "wevpn" // Windscribe is a VPN provider. Windscribe = "windscribe" ) diff --git a/internal/constants/wevpn.go b/internal/constants/wevpn.go new file mode 100644 index 00000000..358a92d4 --- /dev/null +++ b/internal/constants/wevpn.go @@ -0,0 +1,26 @@ +package constants + +import "github.com/qdm12/gluetun/internal/models" + +//nolint:lll +const ( + WevpnCA = "MIIDQjCCAiqgAwIBAgIUPppqnRZfvGGrT4GjXFE4Q29QzgowDQYJKoZIhvcNAQELBQAwEzERMA8GA1UEAwwIQ2hhbmdlTWUwHhcNMTkxMTA1MjMzMzIzWhcNMjkxMTAyMjMzMzIzWjATMREwDwYDVQQDDAhDaGFuZ2VNZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL5DFBJlTqhXukJFWlI8TNW9+HEQCZXhyVFvQhJFF2xIGVNx51XzqxiRANjVJZJrA68kV8az0v2Dxj0SFnRWDR6pOjjdp2CyHFcgHyfv+4MrsreAtkue86bB/1ECPWaoIwtaLnwI6SEmFZl98RlI9v4M/8IE4chOnMrM/F22+2OXI//TduvTcbyOMUiiouIP8UG1FB3J5FyuaW6qPZz2G0efDoaOI+E9LSxE87OoFrII7UqdHlWxRb3nUuPU1Ee4rN/d4tFyP4AvPKfsGhVOwyGG21IdRnbXIuDi0xytkCGOZ4j2bq5zqudnp4Izt6yJgdzZpQQWK3kSHB3qTT/Yzl8CAwEAAaOBjTCBijAdBgNVHQ4EFgQUXYkoo4WbkkvbgLVdGob9RScRf3AwTgYDVR0jBEcwRYAUXYkoo4WbkkvbgLVdGob9RScRf3ChF6QVMBMxETAPBgNVBAMMCENoYW5nZU1lghQ+mmqdFl+8YatPgaNcUThDb1DOCjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAOr1XmyWBRYfTQPNvZZ+DjCfiRYzLOi2AGefZt/jETqPDF8deVbyL1fLhXZzuX+5Etlsil3PflJjpzc/FSeZRuYRaShtwF3j6I08Eww9rBkaCnsukMUcLtMOvhdAU8dUakcRA2wkQ7Z+TWdMBv5+/6MnX10An1fIz7bAy3btMEOPTEFLo8Bst1SxJtUMaqhUteSOJ1VorpK3CWfOFaXxbJAb4E0+3zt6Vsc8mY5tt6wAi8IqiN4WD79ZdvKxENK4FMkR1kNpBY97mvdf82rzpwiBuJgN5ywmH78Ghj+9T8nI6/UIqJ1y22IRYGv6dMif8fHo5WWhCv3qmCqqY8vwuxw==" + WevpnCertificate = "MIIDTDCCAjSgAwIBAgIRAKxt8SMIXezjmHm2KDCAQdIwDQYJKoZIhvcNAQELBQAwEzERMA8GA1UEAwwIQ2hhbmdlTWUwHhcNMTkxMTA1MjMzMzI0WhcNMjkxMTAyMjMzMzI0WjAOMQwwCgYDVQQDDAN0Y3AwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvEwY2erLhMm3Mpsnybm3G6zvGyeblUAaehQVEUs+KM2/5np0Ovx0y8Iz9pIC9ITaWM0B3dM6uBsNEtylZIe4Dd9aFujunSeCFsLRf8i9AbrUombpQ6P4jzYFBxwcEw//UShwa4HZI6JuSYikdpx/dyXdBH2skahwDVc8VUFdBLLSglfKGbuzP9GsdSwQCeBRWgA3dvIzIkQkBwfnt9WQKUfRAe8e5NybaAn8Yuu9sjLkQe6eyV7toxkZTcEXdABG2vtdTEzlAsQilZzIxg3jcdeEgMgRKngng+YNP0rR5nofZ1iDlp+vBj0nuqTTJLHMrRWPIc7bdYFD/f2J49WORAgMBAAGjgZ8wgZwwCQYDVR0TBAIwADAdBgNVHQ4EFgQUmSAFmCo1FAKVq8RQF7jMxMxcMtUwTgYDVR0jBEcwRYAUXYkoo4WbkkvbgLVdGob9RScRf3ChF6QVMBMxETAPBgNVBAMMCENoYW5nZU1lghQ+mmqdFl+8YatPgaNcUThDb1DOCjATBgNVHSUEDDAKBggrBgEFBQcDAjALBgNVHQ8EBAMCB4AwDQYJKoZIhvcNAQELBQADggEBADPqdEgL+0kou8P974QEaNg1XOAXpwP0NNqbkZ/Oj9+Lp96YAhAHOAJig+RWbBktK8zu8oUUGR1qLXAWCmirlXErVuBRnadTEh3A7SOuY02BcsYAtpQ2EU9j5K/LV7nTfagkVdWy7x/av361UD4t9fv1j4YYTh4XLRp7KVXs6AGZ7T1hqPYFMUIoPpFhPzFxH4euJjfazr4SkTR6k6Vhw3pyFd6HP65vcqpzHGxFytSa8HtltBk2DpzIf8yV9TEy+gOXFaaGss0YKQ5OU1ieqZRuLVEGiu17lByYiQGyemIETJbdkyiSg93dDJRxjaTk7c8CEdpipt07ndSIPldMtXA=" + WevpnOpenvpnStaticKeyV1 = "7be66c0df0b8855e076d9e37b19f9ff3c1735ed537dee6dc786e51bdb8502f878077eeba0420a25e2b04814d22bbdcc0191a4fc396fdba1af6eb090a9d8664f18e70012ee98a2e32c28620a771d13cf3a619c417480c2c312562fffaebfd7ba73f57a28edde6c287365e6ce28291a29728da211cb53e01aa46b92f5f276c61fb46bd810b41219022c8f3d9e699fe9ade6bfcbb937fbbf6f49d741740e71c7c008a9a13c2432608038c6310b4f33588d8d234b3dffcf0823395267d73140d0e9a40e323ca92866c37073bfb072ab9de518bb9f2c65df7e219c2f114afbcf7c6e3c401cb08c3ed2901725b0601d2b5de89245719dd32506d52f149d14156215c1e" +) + +func WevpnCityChoices(servers []models.WevpnServer) (choices []string) { + choices = make([]string, len(servers)) + for i := range servers { + choices[i] = servers[i].City + } + return makeUnique(choices) +} + +func WevpnHostnameChoices(servers []models.WevpnServer) (choices []string) { + choices = make([]string, len(servers)) + for i := range servers { + choices[i] = servers[i].Hostname + } + return makeUnique(choices) +} diff --git a/internal/models/getservers.go b/internal/models/getservers.go index 0caf3a68..30b91898 100644 --- a/internal/models/getservers.go +++ b/internal/models/getservers.go @@ -220,6 +220,18 @@ func (a *AllServers) GetVyprvpn() (servers []VyprvpnServer) { return servers } +func (a *AllServers) GetWevpn() (servers []WevpnServer) { + if a.Windscribe.Servers == nil { + return nil + } + servers = make([]WevpnServer, len(a.Wevpn.Servers)) + for i, serverToCopy := range a.Wevpn.Servers { + servers[i] = serverToCopy + servers[i].IPs = copyIPs(serverToCopy.IPs) + } + return servers +} + func (a *AllServers) GetWindscribe() (servers []WindscribeServer) { if a.Windscribe.Servers == nil { return nil diff --git a/internal/models/markdown.go b/internal/models/markdown.go index 180c0004..8a731976 100644 --- a/internal/models/markdown.go +++ b/internal/models/markdown.go @@ -239,6 +239,19 @@ func (s *VyprvpnServer) ToMarkdown() (markdown string) { boolToMarkdown(s.TCP), boolToMarkdown(s.UDP)) } +func (s *WevpnServers) ToMarkdown() (markdown string) { + markdown = markdownTableHeading("City", "Hostname", "TCP", "UDP") + for _, server := range s.Servers { + markdown += server.ToMarkdown() + "\n" + } + return markdown +} + +func (s *WevpnServer) ToMarkdown() (markdown string) { + return fmt.Sprintf("| %s | `%s` | %s | %s |", + s.City, s.Hostname, boolToMarkdown(s.TCP), boolToMarkdown(s.UDP)) +} + func (s *WindscribeServers) ToMarkdown() (markdown string) { markdown = markdownTableHeading("Region", "City", "Hostname", "VPN") for _, server := range s.Servers { diff --git a/internal/models/server.go b/internal/models/server.go index ea63dae4..10147fed 100644 --- a/internal/models/server.go +++ b/internal/models/server.go @@ -157,6 +157,14 @@ type VyprvpnServer struct { IPs []net.IP `json:"ips"` } +type WevpnServer struct { + City string `json:"city"` + Hostname string `json:"hostname"` + TCP bool `json:"tcp"` + UDP bool `json:"udp"` + IPs []net.IP `json:"ips"` +} + type WindscribeServer struct { VPN string `json:"vpn"` Region string `json:"region"` diff --git a/internal/models/servers.go b/internal/models/servers.go index 2a832603..9f242ae8 100644 --- a/internal/models/servers.go +++ b/internal/models/servers.go @@ -18,6 +18,7 @@ type AllServers struct { Torguard TorguardServers `json:"torguard"` VPNUnlimited VPNUnlimitedServers `json:"vpnunlimited"` Vyprvpn VyprvpnServers `json:"vyprvpn"` + Wevpn WevpnServers `json:"wevpn"` Windscribe WindscribeServers `json:"windscribe"` } @@ -38,6 +39,7 @@ func (a *AllServers) Count() int { len(a.Torguard.Servers) + len(a.VPNUnlimited.Servers) + len(a.Vyprvpn.Servers) + + len(a.Wevpn.Servers) + len(a.Windscribe.Servers) } @@ -121,6 +123,11 @@ type VyprvpnServers struct { Timestamp int64 `json:"timestamp"` Servers []VyprvpnServer `json:"servers"` } +type WevpnServers struct { + Version uint16 `json:"version"` + Timestamp int64 `json:"timestamp"` + Servers []WevpnServer `json:"servers"` +} type WindscribeServers struct { Version uint16 `json:"version"` Timestamp int64 `json:"timestamp"` diff --git a/internal/provider/provider.go b/internal/provider/provider.go index c0fa2d7c..49dbe0db 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -28,6 +28,7 @@ import ( "github.com/qdm12/gluetun/internal/provider/torguard" "github.com/qdm12/gluetun/internal/provider/vpnunlimited" "github.com/qdm12/gluetun/internal/provider/vyprvpn" + "github.com/qdm12/gluetun/internal/provider/wevpn" "github.com/qdm12/gluetun/internal/provider/windscribe" "github.com/qdm12/golibs/logging" ) @@ -85,6 +86,8 @@ func New(provider string, allServers models.AllServers, timeNow func() time.Time return vpnunlimited.New(allServers.VPNUnlimited.Servers, randSource) case constants.Vyprvpn: return vyprvpn.New(allServers.Vyprvpn.Servers, randSource) + case constants.Wevpn: + return wevpn.New(allServers.Wevpn.Servers, randSource) case constants.Windscribe: return windscribe.New(allServers.Windscribe.Servers, randSource) default: diff --git a/internal/provider/wevpn/connection.go b/internal/provider/wevpn/connection.go new file mode 100644 index 00000000..5b83cf27 --- /dev/null +++ b/internal/provider/wevpn/connection.go @@ -0,0 +1,43 @@ +package wevpn + +import ( + "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/models" + "github.com/qdm12/gluetun/internal/provider/utils" +) + +func (w *Wevpn) GetConnection(selection configuration.ServerSelection) ( + connection models.Connection, err error) { + port := getPort(selection) + protocol := utils.GetProtocol(selection) + + servers, err := w.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, w.randSource) +} + +func getPort(selection configuration.ServerSelection) (port uint16) { + const ( + defaultOpenVPNTCP = 1195 + defaultOpenVPNUDP = 1194 + defaultWireguard = 0 // Wireguard not supported + ) + return utils.GetPort(selection, defaultOpenVPNTCP, + defaultOpenVPNUDP, defaultWireguard) +} diff --git a/internal/provider/wevpn/connection_test.go b/internal/provider/wevpn/connection_test.go new file mode 100644 index 00000000..1e4a7cf3 --- /dev/null +++ b/internal/provider/wevpn/connection_test.go @@ -0,0 +1,96 @@ +package wevpn + +import ( + "errors" + "math/rand" + "net" + "testing" + + "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/constants" + "github.com/qdm12/gluetun/internal/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_Wevpn_GetConnection(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + servers []models.WevpnServer + selection configuration.ServerSelection + connection models.Connection + err error + }{ + "no server available": { + selection: configuration.ServerSelection{ + VPN: constants.OpenVPN, + }, + err: errors.New("no server found: for VPN openvpn; protocol udp"), + }, + "no filter": { + servers: []models.WevpnServer{ + {UDP: true, IPs: []net.IP{net.IPv4(1, 1, 1, 1)}}, + {UDP: true, IPs: []net.IP{net.IPv4(2, 2, 2, 2)}}, + {UDP: true, IPs: []net.IP{net.IPv4(3, 3, 3, 3)}}, + }, + connection: models.Connection{ + IP: net.IPv4(1, 1, 1, 1), + Port: 1194, + Protocol: constants.UDP, + }, + }, + "target IP": { + selection: configuration.ServerSelection{ + TargetIP: net.IPv4(2, 2, 2, 2), + }, + servers: []models.WevpnServer{ + {UDP: true, IPs: []net.IP{net.IPv4(1, 1, 1, 1)}}, + {UDP: true, IPs: []net.IP{net.IPv4(2, 2, 2, 2)}}, + {UDP: true, IPs: []net.IP{net.IPv4(3, 3, 3, 3)}}, + }, + connection: models.Connection{ + IP: net.IPv4(2, 2, 2, 2), + Port: 1194, + Protocol: constants.UDP, + }, + }, + "with filter": { + selection: configuration.ServerSelection{ + Hostnames: []string{"b"}, + }, + servers: []models.WevpnServer{ + {UDP: true, Hostname: "a", IPs: []net.IP{net.IPv4(1, 1, 1, 1)}}, + {UDP: true, Hostname: "b", IPs: []net.IP{net.IPv4(2, 2, 2, 2)}}, + {UDP: true, Hostname: "a", IPs: []net.IP{net.IPv4(3, 3, 3, 3)}}, + }, + connection: models.Connection{ + IP: net.IPv4(2, 2, 2, 2), + Port: 1194, + Protocol: constants.UDP, + }, + }, + } + + for name, testCase := range testCases { + testCase := testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + + randSource := rand.NewSource(0) + + m := New(testCase.servers, randSource) + + connection, err := m.GetConnection(testCase.selection) + + if testCase.err != nil { + require.Error(t, err) + assert.Equal(t, testCase.err.Error(), err.Error()) + } else { + assert.NoError(t, err) + } + + assert.Equal(t, testCase.connection, connection) + }) + } +} diff --git a/internal/provider/wevpn/filter.go b/internal/provider/wevpn/filter.go new file mode 100644 index 00000000..827cee92 --- /dev/null +++ b/internal/provider/wevpn/filter.go @@ -0,0 +1,27 @@ +package wevpn + +import ( + "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/models" + "github.com/qdm12/gluetun/internal/provider/utils" +) + +func (w *Wevpn) filterServers(selection configuration.ServerSelection) ( + servers []models.WevpnServer, err error) { + for _, server := range w.servers { + switch { + case + utils.FilterByProtocol(selection, server.TCP, server.UDP), + utils.FilterByPossibilities(server.City, selection.Cities), + utils.FilterByPossibilities(server.Hostname, selection.Hostnames): + default: + servers = append(servers, server) + } + } + + if len(servers) == 0 { + return nil, utils.NoServerFoundError(selection) + } + + return servers, nil +} diff --git a/internal/provider/wevpn/filter_test.go b/internal/provider/wevpn/filter_test.go new file mode 100644 index 00000000..3e3c458e --- /dev/null +++ b/internal/provider/wevpn/filter_test.go @@ -0,0 +1,104 @@ +package wevpn + +import ( + "errors" + "math/rand" + "testing" + + "github.com/qdm12/gluetun/internal/configuration" + "github.com/qdm12/gluetun/internal/constants" + "github.com/qdm12/gluetun/internal/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_Wevpn_filterServers(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + servers []models.WevpnServer + selection configuration.ServerSelection + filtered []models.WevpnServer + err error + }{ + "no server available": { + selection: configuration.ServerSelection{ + VPN: constants.OpenVPN, + }, + err: errors.New("no server found: for VPN openvpn; protocol udp"), + }, + "no filter": { + servers: []models.WevpnServer{ + {Hostname: "a", UDP: true}, + {Hostname: "b", UDP: true}, + {Hostname: "c", UDP: true}, + }, + filtered: []models.WevpnServer{ + {Hostname: "a", UDP: true}, + {Hostname: "b", UDP: true}, + {Hostname: "c", UDP: true}, + }, + }, + "filter by protocol": { + selection: configuration.ServerSelection{ + OpenVPN: configuration.OpenVPNSelection{TCP: true}, + }, + servers: []models.WevpnServer{ + {Hostname: "a", UDP: true}, + {Hostname: "b", TCP: true}, + {Hostname: "c", UDP: true}, + }, + filtered: []models.WevpnServer{ + {Hostname: "b", TCP: true}, + }, + }, + "filter by city": { + selection: configuration.ServerSelection{ + Cities: []string{"b"}, + }, + servers: []models.WevpnServer{ + {City: "a", UDP: true}, + {City: "b", UDP: true}, + {City: "c", UDP: true}, + }, + filtered: []models.WevpnServer{ + {City: "b", UDP: true}, + }, + }, + "filter by hostname": { + selection: configuration.ServerSelection{ + Hostnames: []string{"b"}, + }, + servers: []models.WevpnServer{ + {Hostname: "a", UDP: true}, + {Hostname: "b", UDP: true}, + {Hostname: "c", UDP: true}, + }, + filtered: []models.WevpnServer{ + {Hostname: "b", UDP: true}, + }, + }, + } + + for name, testCase := range testCases { + testCase := testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + + randSource := rand.NewSource(0) + + w := New(testCase.servers, randSource) + + servers, err := w.filterServers(testCase.selection) + + if testCase.err != nil { + require.Error(t, err) + assert.Equal(t, testCase.err.Error(), err.Error()) + } else { + assert.NoError(t, err) + } + + assert.Equal(t, testCase.filtered, servers) + }) + } +} diff --git a/internal/provider/wevpn/openvpnconf.go b/internal/provider/wevpn/openvpnconf.go new file mode 100644 index 00000000..f3d80757 --- /dev/null +++ b/internal/provider/wevpn/openvpnconf.go @@ -0,0 +1,84 @@ +package wevpn + +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 (w *Wevpn) BuildConf(connection models.Connection, + settings configuration.OpenVPN) (lines []string, err error) { + if settings.Cipher == "" { + settings.Cipher = constants.AES256gcm + } + + if settings.Auth == "" { + settings.Auth = constants.SHA512 + } + + lines = []string{ + "client", + "nobind", + "tls-exit", + "dev " + settings.Interface, + "verb " + strconv.Itoa(settings.Verbosity), + + // Wevpn specific + "ping 30", + "remote-cert-tls server", + "redirect-gateway def1 bypass-dhcp", + "reneg-sec 0", + "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(), + } + + if connection.Protocol == constants.UDP { + lines = append(lines, "explicit-exit-notify") + } + + lines = append(lines, utils.CipherLines(settings.Cipher, settings.Version)...) + + if !settings.Root { + lines = append(lines, "user "+settings.ProcUser) + lines = append(lines, "persist-tun") + lines = append(lines, "persist-key") + } + + if settings.MSSFix > 0 { + lines = append(lines, "mssfix "+strconv.Itoa(int(settings.MSSFix))) + } + + if settings.IPv6 { + lines = append(lines, "tun-ipv6") + } else { + lines = append(lines, `pull-filter ignore "route-ipv6"`) + lines = append(lines, `pull-filter ignore "ifconfig-ipv6"`) + } + + lines = append(lines, utils.WrapOpenvpnKey( + settings.ClientKey)...) + lines = append(lines, utils.WrapOpenvpnCA( + constants.WevpnCA)...) + lines = append(lines, utils.WrapOpenvpnCert( + constants.WevpnCertificate)...) + lines = append(lines, utils.WrapOpenvpnTLSCrypt( + constants.WevpnOpenvpnStaticKeyV1)...) + + lines = append(lines, "") + + return lines, nil +} diff --git a/internal/provider/wevpn/provider.go b/internal/provider/wevpn/provider.go new file mode 100644 index 00000000..64c97c87 --- /dev/null +++ b/internal/provider/wevpn/provider.go @@ -0,0 +1,23 @@ +package wevpn + +import ( + "math/rand" + + "github.com/qdm12/gluetun/internal/constants" + "github.com/qdm12/gluetun/internal/models" + "github.com/qdm12/gluetun/internal/provider/utils" +) + +type Wevpn struct { + servers []models.WevpnServer + randSource rand.Source + utils.NoPortForwarder +} + +func New(servers []models.WevpnServer, randSource rand.Source) *Wevpn { + return &Wevpn{ + servers: servers, + randSource: randSource, + NoPortForwarder: utils.NewNoPortForwarding(constants.Wevpn), + } +} diff --git a/internal/storage/hardcoded_test.go b/internal/storage/hardcoded_test.go index 3ac96e27..b771cab8 100644 --- a/internal/storage/hardcoded_test.go +++ b/internal/storage/hardcoded_test.go @@ -128,6 +128,11 @@ func Test_versions(t *testing.T) { version: allServers.Vyprvpn.Version, digest: "58de06d8", }, + "Wevpn": { + model: models.WevpnServer{}, + version: allServers.Wevpn.Version, + digest: "f4daa186", + }, "Windscribe": { model: models.WindscribeServer{}, version: allServers.Windscribe.Version, diff --git a/internal/storage/merge.go b/internal/storage/merge.go index 92072cc2..c5f91fb2 100644 --- a/internal/storage/merge.go +++ b/internal/storage/merge.go @@ -49,6 +49,7 @@ func (s *Storage) mergeServers(hardcoded, persisted models.AllServers) models.Al 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), } } @@ -279,6 +280,21 @@ func (s *Storage) mergeVyprvpn(hardcoded, persisted models.VyprvpnServers) model return persisted } +func (s *Storage) mergeWevpn(hardcoded, persisted models.WevpnServers) models.WevpnServers { + if persisted.Timestamp <= hardcoded.Timestamp { + return hardcoded + } + + versionDiff := int(hardcoded.Version) - int(persisted.Version) + if versionDiff > 0 { + s.logVersionDiff("WeVPN", versionDiff) + return hardcoded + } + + s.logTimeDiff("WeVPN", persisted.Timestamp, hardcoded.Timestamp) + return persisted +} + func (s *Storage) mergeWindscribe(hardcoded, persisted models.WindscribeServers) models.WindscribeServers { if persisted.Timestamp <= hardcoded.Timestamp { return hardcoded diff --git a/internal/storage/servers.json b/internal/storage/servers.json index 572ed5eb..c1ebd849 100644 --- a/internal/storage/servers.json +++ b/internal/storage/servers.json @@ -107914,6 +107914,633 @@ } ] }, + "wevpn": { + "version": 1, + "timestamp": 1632407719, + "servers": [ + { + "city": "Amsterdam", + "hostname": "amsterdam.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "89.238.177.234" + ] + }, + { + "city": "Athens", + "hostname": "athens.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "185.51.134.210" + ] + }, + { + "city": "Atlanta", + "hostname": "atlanta.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "104.223.91.146" + ] + }, + { + "city": "Auckland", + "hostname": "auckland.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "103.108.94.226" + ] + }, + { + "city": "Belgrade", + "hostname": "belgrade.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "37.120.193.170" + ] + }, + { + "city": "Brussels", + "hostname": "brussels.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "77.243.191.178" + ] + }, + { + "city": "Bucharest", + "hostname": "bucharest.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "86.105.25.162" + ] + }, + { + "city": "Budapest", + "hostname": "budapest.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "185.128.26.130" + ] + }, + { + "city": "Buenos Aires", + "hostname": "buenosaires.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "131.255.4.140" + ] + }, + { + "city": "Cairo", + "hostname": "cairo.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "188.214.122.138" + ] + }, + { + "city": "Chennai", + "hostname": "chennai.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "169.38.107.52" + ] + }, + { + "city": "Chicago", + "hostname": "chicago.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "69.39.239.57" + ] + }, + { + "city": "Copenhagen", + "hostname": "copenhagen.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "37.120.194.82" + ] + }, + { + "city": "Dallas", + "hostname": "dallas.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "194.110.112.74" + ] + }, + { + "city": "Denizli", + "hostname": "bursa.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "95.173.161.240" + ] + }, + { + "city": "Denver", + "hostname": "denver.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "70.39.71.2" + ] + }, + { + "city": "Dubai", + "hostname": "dubai.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "217.138.193.42" + ] + }, + { + "city": "Dublin", + "hostname": "dublin.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "217.138.222.138" + ] + }, + { + "city": "Frankfurt", + "hostname": "frankfurt.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "45.141.152.178" + ] + }, + { + "city": "Hanoi", + "hostname": "hanoi.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "188.214.152.194" + ] + }, + { + "city": "Helsinki", + "hostname": "helsinki.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "185.212.149.152" + ] + }, + { + "city": "Hong Kong", + "hostname": "hongkong.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "84.17.37.55" + ] + }, + { + "city": "Jakarta", + "hostname": "jakarta.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "45.133.181.58" + ] + }, + { + "city": "Johannesburg", + "hostname": "johannesburg.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "102.165.20.38" + ] + }, + { + "city": "Kiev", + "hostname": "kyiv.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "37.19.218.197" + ] + }, + { + "city": "Lagos", + "hostname": "lagos.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "102.129.144.142" + ] + }, + { + "city": "Lisbon", + "hostname": "lisbon.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "185.90.57.152" + ] + }, + { + "city": "London", + "hostname": "london.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "45.141.154.2" + ] + }, + { + "city": "London-PF", + "hostname": "london-pf.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "77.243.177.78" + ] + }, + { + "city": "Los Angeles", + "hostname": "losangeles.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "45.152.182.82" + ] + }, + { + "city": "Los Angeles-PF", + "hostname": "losangeles-pf.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "66.55.92.75" + ] + }, + { + "city": "Luxembourg", + "hostname": "luxembourg.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "5.253.204.194" + ] + }, + { + "city": "Madrid", + "hostname": "madrid.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "45.152.183.242" + ] + }, + { + "city": "Manchester", + "hostname": "manchester.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "86.106.136.98" + ] + }, + { + "city": "Manila", + "hostname": "manila.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "188.214.125.106" + ] + }, + { + "city": "Melbourne", + "hostname": "melbourne.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "116.206.230.130" + ] + }, + { + "city": "Mexico City", + "hostname": "mexico.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "169.57.35.97" + ] + }, + { + "city": "Miami", + "hostname": "miami.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "96.47.224.2" + ] + }, + { + "city": "Milan", + "hostname": "milan.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "217.138.197.138" + ] + }, + { + "city": "Montreal", + "hostname": "montreal.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "217.138.200.242" + ] + }, + { + "city": "Moscow", + "hostname": "moscow.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "80.93.181.194" + ] + }, + { + "city": "New Jersey", + "hostname": "newjersey.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "23.226.131.146" + ] + }, + { + "city": "New York", + "hostname": "newyork.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "217.138.206.106" + ] + }, + { + "city": "New York-PF", + "hostname": "newyork-pf.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "188.241.179.2" + ] + }, + { + "city": "Oslo", + "hostname": "oslo.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "84.247.50.10" + ] + }, + { + "city": "Oulu", + "hostname": "oulu.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "188.126.89.174" + ] + }, + { + "city": "Paris", + "hostname": "paris.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "37.120.136.234" + ] + }, + { + "city": "Phoenix", + "hostname": "phoenix.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "170.130.15.34" + ] + }, + { + "city": "Prague", + "hostname": "prague.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "217.138.199.138" + ] + }, + { + "city": "Salt Lake City", + "hostname": "saltlakecity.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "107.182.234.233" + ] + }, + { + "city": "San Jose", + "hostname": "sanjose.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "169.62.109.140" + ] + }, + { + "city": "Sao Paulo", + "hostname": "saopaulo.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "177.54.152.89" + ] + }, + { + "city": "Seattle", + "hostname": "seattle.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "104.140.21.178" + ] + }, + { + "city": "Seoul", + "hostname": "seoul.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "169.56.83.206" + ] + }, + { + "city": "Sibu", + "hostname": "kualalumpur.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "223.25.246.4" + ] + }, + { + "city": "Singapore", + "hostname": "singapore.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "84.17.38.144" + ] + }, + { + "city": "Sofia", + "hostname": "sofia.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "217.138.221.42" + ] + }, + { + "city": "St Petersburg", + "hostname": "petersburg.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "82.202.220.242" + ] + }, + { + "city": "Stockholm", + "hostname": "stockholm.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "45.83.91.66" + ] + }, + { + "city": "Sydney", + "hostname": "sydney.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "46.102.153.106" + ] + }, + { + "city": "Taipei", + "hostname": "taipei.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "188.214.106.98" + ] + }, + { + "city": "Tel Aviv", + "hostname": "telaviv.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "193.43.72.242" + ] + }, + { + "city": "Tokyo", + "hostname": "tokyo.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "84.17.34.8" + ] + }, + { + "city": "Toronto", + "hostname": "toronto.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "184.75.208.234" + ] + }, + { + "city": "Vancouver", + "hostname": "vancouver.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "71.19.249.109" + ] + }, + { + "city": "Vienna", + "hostname": "vienna.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "37.120.212.162" + ] + }, + { + "city": "Warsaw", + "hostname": "warsaw.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "217.138.209.122" + ] + }, + { + "city": "Washington DC", + "hostname": "washington.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "70.32.0.208" + ] + }, + { + "city": "Zurich", + "hostname": "zurich.wevpn.com", + "tcp": false, + "udp": true, + "ips": [ + "37.120.137.82" + ] + } + ] + }, "windscribe": { "version": 1, "timestamp": 1629420754, @@ -115123,4 +115750,4 @@ } ] } -} \ No newline at end of file +} diff --git a/internal/storage/sync.go b/internal/storage/sync.go index e849e3cd..b2e4db30 100644 --- a/internal/storage/sync.go +++ b/internal/storage/sync.go @@ -30,6 +30,7 @@ func countServers(allServers models.AllServers) int { len(allServers.Torguard.Servers) + len(allServers.VPNUnlimited.Servers) + len(allServers.Vyprvpn.Servers) + + len(allServers.Wevpn.Servers) + len(allServers.Windscribe.Servers) } diff --git a/internal/updater/providers.go b/internal/updater/providers.go index e857a43c..81ef209c 100644 --- a/internal/updater/providers.go +++ b/internal/updater/providers.go @@ -22,6 +22,7 @@ import ( "github.com/qdm12/gluetun/internal/updater/providers/torguard" "github.com/qdm12/gluetun/internal/updater/providers/vpnunlimited" "github.com/qdm12/gluetun/internal/updater/providers/vyprvpn" + "github.com/qdm12/gluetun/internal/updater/providers/wevpn" "github.com/qdm12/gluetun/internal/updater/providers/windscribe" ) @@ -357,6 +358,27 @@ func (u *updater) updateVyprvpn(ctx context.Context) (err error) { return nil } +func (u *updater) updateWevpn(ctx context.Context) (err error) { + minServers := getMinServers(len(u.servers.Wevpn.Servers)) + servers, warnings, err := wevpn.GetServers(ctx, u.presolver, minServers) + if u.options.CLI { + for _, warning := range warnings { + u.logger.Warn("WeVPN: " + warning) + } + } + if err != nil { + return err + } + + if reflect.DeepEqual(u.servers.Wevpn.Servers, servers) { + return nil + } + + u.servers.Wevpn.Timestamp = u.timeNow().Unix() + u.servers.Wevpn.Servers = servers + return nil +} + func (u *updater) updateWindscribe(ctx context.Context) (err error) { minServers := getMinServers(len(u.servers.Windscribe.Servers)) servers, err := windscribe.GetServers(ctx, u.client, minServers) diff --git a/internal/updater/providers/wevpn/cities.go b/internal/updater/providers/wevpn/cities.go new file mode 100644 index 00000000..1d139e59 --- /dev/null +++ b/internal/updater/providers/wevpn/cities.go @@ -0,0 +1,76 @@ +package wevpn + +// getAvailableCities get available cities as listed on the WeVPN website. +func getAvailableCities() (cities []string) { + return []string{ + "Cairo", + "Chennai", + "Denizli", + "Dubai", + "Johannesburg", + "Lagos", + "Tel Aviv", + "Atlanta", + "Buenos Aires", + "Chicago", + "Dallas", + "Denver", + "Los Angeles", + "Los Angeles-PF", + "Mexico City", + "Miami", + "Montreal", + "New Jersey", + "New York", + "New York-PF", + "Phoenix", + "Salt Lake City", + "San Jose", + "Sao Paulo", + "Seattle", + "Toronto", + "Vancouver", + "Washington DC", + "Auckland", + "Hanoi", + "Hong Kong", + "Jakarta", + "Manila", + "Melbourne", + "Moscow", + "Seoul", + "Sibu", + "Singapore", + "St Petersburg", + "Sydney", + "Taipei", + "Tokyo", + "Amsterdam", + "Athens", + "Belgrade", + "Brussels", + "Bucharest", + "Budapest", + "Copenhagen", + "Dublin", + "Frankfurt", + "Helsinki", + "Kiev", + "Lisbon", + "London", + "London-PF", + "Luxembourg", + "Madrid", + "Manchester", + "Milan", + "Oslo", + "Oulu", + "Paris", + "Prague", + "Sofia", + "Stockholm", + "Vienna", + "Warsaw", + "Zurich", + } +} diff --git a/internal/updater/providers/wevpn/hostname.go b/internal/updater/providers/wevpn/hostname.go new file mode 100644 index 00000000..61ac4291 --- /dev/null +++ b/internal/updater/providers/wevpn/hostname.go @@ -0,0 +1,24 @@ +package wevpn + +import "strings" + +func getHostnameFromCity(city string) (hostname string) { + host := strings.ToLower(city) + host = strings.ReplaceAll(host, ".", "") + host = strings.ReplaceAll(host, " ", "") + + specialCases := map[string]string{ + "washingtondc": "washington", + "mexicocity": "mexico", + "denizli": "bursa", + "sibu": "kualalumpur", + "kiev": "kyiv", + "stpetersburg": "petersburg", + } + if specialHost, ok := specialCases[host]; ok { + host = specialHost + } + + hostname = host + ".wevpn.com" + return hostname +} diff --git a/internal/updater/providers/wevpn/resolve.go b/internal/updater/providers/wevpn/resolve.go new file mode 100644 index 00000000..e9d653b4 --- /dev/null +++ b/internal/updater/providers/wevpn/resolve.go @@ -0,0 +1,33 @@ +package wevpn + +import ( + "context" + "net" + "time" + + "github.com/qdm12/gluetun/internal/updater/resolver" +) + +func resolveHosts(ctx context.Context, presolver resolver.Parallel, + hosts []string, minServers int) (hostToIPs map[string][]net.IP, + warnings []string, err error) { + const ( + maxFailRatio = 0.1 + maxDuration = 20 * time.Second + betweenDuration = time.Second + maxNoNew = 2 + maxFails = 2 + ) + settings := resolver.ParallelSettings{ + MaxFailRatio: maxFailRatio, + MinFound: minServers, + Repeat: resolver.RepeatSettings{ + MaxDuration: maxDuration, + BetweenDuration: betweenDuration, + MaxNoNew: maxNoNew, + MaxFails: maxFails, + SortIPs: true, + }, + } + return presolver.Resolve(ctx, hosts, settings) +} diff --git a/internal/updater/providers/wevpn/resolve_test.go b/internal/updater/providers/wevpn/resolve_test.go new file mode 100644 index 00000000..e18a471b --- /dev/null +++ b/internal/updater/providers/wevpn/resolve_test.go @@ -0,0 +1,57 @@ +package wevpn + +import ( + "context" + "errors" + "net" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/qdm12/gluetun/internal/updater/resolver" + "github.com/qdm12/gluetun/internal/updater/resolver/mock_resolver" + "github.com/stretchr/testify/assert" +) + +func Test_resolveHosts(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + + ctx := context.Background() + presolver := mock_resolver.NewMockParallel(ctrl) + hosts := []string{"host1", "host2"} + const minServers = 10 + + expectedHostToIPs := map[string][]net.IP{ + "host1": {{1, 2, 3, 4}}, + "host2": {{2, 3, 4, 5}}, + } + expectedWarnings := []string{"warning1", "warning2"} + expectedErr := errors.New("dummy") + + const ( + maxFailRatio = 0.1 + maxDuration = 20 * time.Second + betweenDuration = time.Second + maxNoNew = 2 + maxFails = 2 + ) + expectedSettings := resolver.ParallelSettings{ + MaxFailRatio: maxFailRatio, + MinFound: minServers, + Repeat: resolver.RepeatSettings{ + MaxDuration: maxDuration, + BetweenDuration: betweenDuration, + MaxNoNew: maxNoNew, + MaxFails: maxFails, + SortIPs: true, + }, + } + presolver.EXPECT().Resolve(ctx, hosts, expectedSettings). + Return(expectedHostToIPs, expectedWarnings, expectedErr) + + hostToIPs, warnings, err := resolveHosts(ctx, presolver, hosts, minServers) + assert.Equal(t, expectedHostToIPs, hostToIPs) + assert.Equal(t, expectedWarnings, warnings) + assert.Equal(t, expectedErr, err) +} diff --git a/internal/updater/providers/wevpn/servers.go b/internal/updater/providers/wevpn/servers.go new file mode 100644 index 00000000..2067d35c --- /dev/null +++ b/internal/updater/providers/wevpn/servers.go @@ -0,0 +1,58 @@ +// package wevpn contains code to obtain the server information +// for the WeVPN provider. +package wevpn + +import ( + "context" + "errors" + "fmt" + + "github.com/qdm12/gluetun/internal/models" + "github.com/qdm12/gluetun/internal/updater/resolver" +) + +var ( + ErrGetZip = errors.New("cannot get OpenVPN ZIP file") + ErrGetAPI = errors.New("cannot fetch server information from API") + ErrNotEnoughServers = errors.New("not enough servers found") +) + +func GetServers(ctx context.Context, presolver resolver.Parallel, minServers int) ( + servers []models.WevpnServer, warnings []string, err error) { + cities := getAvailableCities() + servers = make([]models.WevpnServer, 0, len(cities)) + hostnames := make([]string, len(cities)) + hostnameToCity := make(map[string]string, len(cities)) + + for i, city := range cities { + hostname := getHostnameFromCity(city) + hostnames[i] = hostname + hostnameToCity[hostname] = city + } + + hostnameToIPs, newWarnings, err := resolveHosts(ctx, presolver, hostnames, minServers) + warnings = append(warnings, newWarnings...) + if err != nil { + return nil, warnings, err + } + + if len(hostnameToIPs) < minServers { + return nil, warnings, fmt.Errorf("%w: %d and expected at least %d", + ErrNotEnoughServers, len(servers), minServers) + } + + for hostname, ips := range hostnameToIPs { + city := hostnameToCity[hostname] + server := models.WevpnServer{ + City: city, + Hostname: hostname, + UDP: true, + IPs: ips, + } + servers = append(servers, server) + } + + sortServers(servers) + + return servers, warnings, nil +} diff --git a/internal/updater/providers/wevpn/sort.go b/internal/updater/providers/wevpn/sort.go new file mode 100644 index 00000000..2fd6d707 --- /dev/null +++ b/internal/updater/providers/wevpn/sort.go @@ -0,0 +1,16 @@ +package wevpn + +import ( + "sort" + + "github.com/qdm12/gluetun/internal/models" +) + +func sortServers(servers []models.WevpnServer) { + sort.Slice(servers, func(i, j int) bool { + if servers[i].City == servers[j].City { + return servers[i].Hostname < servers[j].Hostname + } + return servers[i].City < servers[j].City + }) +} diff --git a/internal/updater/providers/wevpn/sort_test.go b/internal/updater/providers/wevpn/sort_test.go new file mode 100644 index 00000000..9f44d20e --- /dev/null +++ b/internal/updater/providers/wevpn/sort_test.go @@ -0,0 +1,40 @@ +package wevpn + +import ( + "testing" + + "github.com/qdm12/gluetun/internal/models" + "github.com/stretchr/testify/assert" +) + +func Test_sortServers(t *testing.T) { + t.Parallel() + testCases := map[string]struct { + initialServers []models.WevpnServer + sortedServers []models.WevpnServer + }{ + "no server": {}, + "sorted servers": { + initialServers: []models.WevpnServer{ + {City: "A", Hostname: "A"}, + {City: "A", Hostname: "B"}, + {City: "A", Hostname: "A"}, + {City: "B", Hostname: "A"}, + }, + sortedServers: []models.WevpnServer{ + {City: "A", Hostname: "A"}, + {City: "A", Hostname: "A"}, + {City: "A", Hostname: "B"}, + {City: "B", Hostname: "A"}, + }, + }, + } + for name, testCase := range testCases { + testCase := testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + sortServers(testCase.initialServers) + assert.Equal(t, testCase.sortedServers, testCase.initialServers) + }) + } +} diff --git a/internal/updater/updater.go b/internal/updater/updater.go index 2c1575a6..e11bd53f 100644 --- a/internal/updater/updater.go +++ b/internal/updater/updater.go @@ -214,6 +214,16 @@ func (u *updater) UpdateServers(ctx context.Context) (allServers models.AllServe } } + if u.options.Wevpn { + u.logger.Info("updating WeVPN servers...") + if err := u.updateWevpn(ctx); err != nil { + if ctxErr := ctx.Err(); ctxErr != nil { + return allServers, ctxErr + } + u.logger.Error(err.Error()) + } + } + if u.options.Windscribe { u.logger.Info("updating Windscribe servers...") if err := u.updateWindscribe(ctx); err != nil {