Feature: Protonvpn support (#437 clone on #434)

This commit is contained in:
Quentin McGaw
2021-04-25 15:44:45 -04:00
committed by GitHub
parent b02a80abbd
commit 954e3c70b2
22 changed files with 2209 additions and 10 deletions

2
.github/labels.yml vendored
View File

@@ -39,6 +39,8 @@
- name: ":cloud: PrivateVPN"
color: "cfe8d4"
description: ""
- name: ":cloud: ProtonVPN"
color: "cfe8d4"
- name: ":cloud: PureVPN"
color: "cfe8d4"
description: ""

View File

@@ -2,7 +2,7 @@
*Lightweight swiss-knife-like VPN client to tunnel to Cyberghost, FastestVPN,
HideMyAss, Mullvad, NordVPN, Privado, Private Internet Access, PrivateVPN,
PureVPN, Surfshark, TorGuard, VyprVPN and Windscribe VPN servers
ProtonVPN, PureVPN, Surfshark, TorGuard, VyprVPN and Windscribe VPN servers
using Go, OpenVPN, iptables, DNS over TLS, ShadowSocks and an HTTP proxy*
**ANNOUNCEMENT**:
@@ -39,7 +39,7 @@ using Go, OpenVPN, iptables, DNS over TLS, ShadowSocks and an HTTP proxy*
## Features
- Based on Alpine 3.13 for a small Docker image of 52MB
- Supports: **Cyberghost**, **FastestVPN**, **HideMyAss**, **Mullvad**, **NordVPN**, **Privado**, **Private Internet Access**, **PrivateVPN**, **PureVPN**, **Surfshark**, **TorGuard**, **Vyprvpn**, **Windscribe**, servers
- Supports: **Cyberghost**, **FastestVPN**, **HideMyAss**, **Mullvad**, **NordVPN**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **Surfshark**, **TorGuard**, **Vyprvpn**, **Windscribe** servers
- Supports Openvpn only for now
- DNS over TLS baked in with service provider(s) of your choice
- DNS fine blocking of malicious/ads/surveillance hostnames and IP addresses, with live update every 24 hours

View File

@@ -30,6 +30,7 @@ func (c *cli) Update(ctx context.Context, args []string, os os.OS) error {
flagSet.BoolVar(&options.PIA, "pia", false, "Update Private Internet Access post-summer 2020 servers")
flagSet.BoolVar(&options.Privado, "privado", false, "Update Privado servers")
flagSet.BoolVar(&options.Privatevpn, "privatevpn", false, "Update Private VPN servers")
flagSet.BoolVar(&options.Protonvpn, "protonvpn", false, "Update Protonvpn servers")
flagSet.BoolVar(&options.Purevpn, "purevpn", false, "Update Purevpn servers")
flagSet.BoolVar(&options.Surfshark, "surfshark", false, "Update Surfshark servers")
flagSet.BoolVar(&options.Torguard, "torguard", false, "Update Torguard servers")

View File

@@ -62,7 +62,7 @@ var (
func (settings *OpenVPN) read(r reader) (err error) {
vpnsp, err := r.env.Inside("VPNSP", []string{
"cyberghost", "fastestvpn", "hidemyass", "mullvad", "nordvpn",
"privado", "pia", "private internet access", "privatevpn",
"privado", "pia", "private internet access", "privatevpn", "protonvpn",
"purevpn", "surfshark", "torguard", "vyprvpn", "windscribe"},
params.Default("private internet access"))
if err != nil {
@@ -141,6 +141,8 @@ func (settings *OpenVPN) read(r reader) (err error) {
readProvider = settings.Provider.readPrivateInternetAccess
case constants.Privatevpn:
readProvider = settings.Provider.readPrivatevpn
case constants.Protonvpn:
readProvider = settings.Provider.readProtonvpn
case constants.Purevpn:
readProvider = settings.Provider.readPurevpn
case constants.Surfshark:

View File

@@ -16,10 +16,43 @@ func Test_OpenVPN_JSON(t *testing.T) {
Name: "name",
},
}
data, err := json.Marshal(in)
data, err := json.MarshalIndent(in, "", " ")
require.NoError(t, err)
//nolint:lll
assert.Equal(t, `{"user":"","password":"","verbosity":0,"mssfix":0,"run_as_root":true,"cipher":"","auth":"","provider":{"name":"name","server_selection":{"network_protocol":"","regions":null,"group":"","countries":null,"cities":null,"hostnames":null,"isps":null,"owned":false,"custom_port":0,"numbers":null,"encryption_preset":""},"extra_config":{"encryption_preset":"","openvpn_ipv6":false},"port_forwarding":{"enabled":false,"filepath":""}},"custom_config":""}`, string(data))
assert.Equal(t, `{
"user": "",
"password": "",
"verbosity": 0,
"mssfix": 0,
"run_as_root": true,
"cipher": "",
"auth": "",
"provider": {
"name": "name",
"server_selection": {
"network_protocol": "",
"regions": null,
"group": "",
"countries": null,
"cities": null,
"hostnames": null,
"names": null,
"isps": null,
"owned": false,
"custom_port": 0,
"numbers": null,
"encryption_preset": ""
},
"extra_config": {
"encryption_preset": "",
"openvpn_ipv6": false
},
"port_forwarding": {
"enabled": false,
"filepath": ""
}
},
"custom_config": ""
}`, string(data))
var out OpenVPN
err = json.Unmarshal(data, &out)
require.NoError(t, err)

View File

@@ -0,0 +1,75 @@
package configuration
import (
"github.com/qdm12/gluetun/internal/constants"
)
func (settings *Provider) protonvpnLines() (lines []string) {
if len(settings.ServerSelection.Countries) > 0 {
lines = append(lines, lastIndent+"Countries: "+commaJoin(settings.ServerSelection.Countries))
}
if len(settings.ServerSelection.Regions) > 0 {
lines = append(lines, lastIndent+"Regions: "+commaJoin(settings.ServerSelection.Regions))
}
if len(settings.ServerSelection.Cities) > 0 {
lines = append(lines, lastIndent+"Cities: "+commaJoin(settings.ServerSelection.Cities))
}
if len(settings.ServerSelection.Names) > 0 {
lines = append(lines, lastIndent+"Names: "+commaJoin(settings.ServerSelection.Names))
}
if len(settings.ServerSelection.Hostnames) > 0 {
lines = append(lines, lastIndent+"Hostnames: "+commaJoin(settings.ServerSelection.Hostnames))
}
return lines
}
func (settings *Provider) readProtonvpn(r reader) (err error) {
settings.Name = constants.Protonvpn
settings.ServerSelection.Protocol, err = readProtocol(r.env)
if err != nil {
return err
}
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
if err != nil {
return err
}
settings.ServerSelection.CustomPort, err = readPortOrZero(r.env, "PORT")
if err != nil {
return err
}
settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.ProtonvpnCountryChoices())
if err != nil {
return err
}
settings.ServerSelection.Regions, err = r.env.CSVInside("REGION", constants.ProtonvpnRegionChoices())
if err != nil {
return err
}
settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.ProtonvpnCityChoices())
if err != nil {
return err
}
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_NAME", constants.ProtonvpnNameChoices())
if err != nil {
return err
}
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.ProtonvpnHostnameChoices())
if err != nil {
return err
}
return nil
}

View File

@@ -45,6 +45,8 @@ func (settings *Provider) lines() (lines []string) {
providerLines = settings.privatevpnLines()
case "private internet access":
providerLines = settings.privateinternetaccessLines()
case "protonvpn":
providerLines = settings.protonvpnLines()
case "purevpn":
providerLines = settings.purevpnLines()
case "surfshark":

View File

@@ -148,6 +148,28 @@ func Test_Provider_lines(t *testing.T) {
" |--Hostnames: a, b",
},
},
"protonvpn": {
settings: Provider{
Name: constants.Protonvpn,
ServerSelection: ServerSelection{
Protocol: constants.UDP,
Countries: []string{"a", "b"},
Regions: []string{"c", "d"},
Cities: []string{"e", "f"},
Names: []string{"g", "h"},
Hostnames: []string{"i", "j"},
},
},
lines: []string{
"|--Protonvpn settings:",
" |--Network protocol: udp",
" |--Countries: a, b",
" |--Regions: c, d",
" |--Cities: e, f",
" |--Names: g, h",
" |--Hostnames: i, j",
},
},
"private internet access": {
settings: Provider{
Name: constants.PrivateInternetAccess,

View File

@@ -9,15 +9,16 @@ type ServerSelection struct {
Protocol string `json:"network_protocol"`
TargetIP net.IP `json:"target_ip,omitempty"`
// TODO comments
// Cyberghost, PIA, Surfshark, Windscribe, Vyprvpn, NordVPN
// Cyberghost, PIA, Protonvpn, Surfshark, Windscribe, Vyprvpn, NordVPN
Regions []string `json:"regions"`
// Cyberghost
Group string `json:"group"`
Countries []string `json:"countries"` // Fastestvpn, HideMyAss, Mullvad, PrivateVPN, PureVPN
Cities []string `json:"cities"` // HideMyAss, Mullvad, PrivateVPN, PureVPN, Windscribe
Hostnames []string `json:"hostnames"` // Fastestvpn, HideMyAss, PrivateVPN, Windscribe, Privado
Countries []string `json:"countries"` // Fastestvpn, HideMyAss, Mullvad, PrivateVPN, Protonvpn, PureVPN
Cities []string `json:"cities"` // HideMyAss, Mullvad, PrivateVPN, Protonvpn, PureVPN, Windscribe
Hostnames []string `json:"hostnames"` // Fastestvpn, HideMyAss, PrivateVPN, Windscribe, Privado, Protonvpn
Names []string `json:"names"` // Protonvpn
// Mullvad
ISPs []string `json:"isps"`

View File

@@ -18,6 +18,7 @@ type Updater struct {
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"`
@@ -53,6 +54,7 @@ func (settings *Updater) read(r reader) (err error) {
settings.PIA = true
settings.Privado = true
settings.Privatevpn = true
settings.Protonvpn = true
settings.Purevpn = true
settings.Surfshark = true
settings.Torguard = true

File diff suppressed because it is too large Load Diff

View File

@@ -41,6 +41,11 @@ func GetAllServers() (allServers models.AllServers) {
Timestamp: 1613861528,
Servers: PrivatevpnServers(),
},
Protonvpn: models.ProtonvpnServers{
Version: 1,
Timestamp: 1618605078,
Servers: ProtonvpnServers(),
},
Pia: models.PiaServers{
Version: 4,
Timestamp: 1619272345,

View File

@@ -74,6 +74,11 @@ func Test_versions(t *testing.T) {
version: allServers.Privatevpn.Version,
digest: "cba13d78",
},
"Protonvpn": {
model: models.ProtonvpnServer{},
version: allServers.Protonvpn.Version,
digest: "b964085b",
},
"Purevpn": {
model: models.PurevpnServer{},
version: allServers.Purevpn.Version,
@@ -175,6 +180,11 @@ func Test_timestamps(t *testing.T) {
timestamp: allServers.Privatevpn.Timestamp,
digest: "8ce3fba1",
},
"Protonvpn": {
servers: allServers.Protonvpn.Servers,
timestamp: allServers.Protonvpn.Timestamp,
digest: "c342020e",
},
"Purevpn": {
servers: allServers.Purevpn.Servers,
timestamp: allServers.Purevpn.Timestamp,

View File

@@ -17,6 +17,8 @@ const (
PrivateInternetAccess = "private internet access"
// Privatevpn is a VPN provider.
Privatevpn = "privatevpn"
// Protonvpn is a VPN provider.
Protonvpn = "protonvpn"
// PureVPN is a VPN provider.
Purevpn = "purevpn"
// Surfshark is a VPN provider.

View File

@@ -108,6 +108,21 @@ func (s *PrivatevpnServer) String() string {
s.Country, s.City, s.Hostname, goStringifyIPs(s.IPs))
}
type ProtonvpnServer struct {
Country string `json:"country"`
Region string `json:"region"`
City string `json:"city"`
Name string `json:"name"`
Hostname string `json:"hostname"`
EntryIP net.IP `json:"entry_ip"`
ExitIP net.IP `json:"exit_ip"` // TODO verify it matches with public IP once connected
}
func (s *ProtonvpnServer) String() string {
return fmt.Sprintf("{Country: %q, Region: %q, City: %q, Name: %q, Hostname: %q, EntryIP: %s, ExitIP: %s}",
s.Country, s.Region, s.City, s.Name, s.Hostname, goStringifyIP(s.EntryIP), goStringifyIP(s.ExitIP))
}
type PurevpnServer struct {
Country string `json:"country"`
Region string `json:"region"`

View File

@@ -10,6 +10,7 @@ type AllServers struct {
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"`
@@ -26,6 +27,7 @@ func (a *AllServers) Count() int {
len(a.Privado.Servers) +
len(a.Pia.Servers) +
len(a.Privatevpn.Servers) +
len(a.Protonvpn.Servers) +
len(a.Purevpn.Servers) +
len(a.Surfshark.Servers) +
len(a.Torguard.Servers) +
@@ -73,6 +75,11 @@ type PrivatevpnServers struct {
Timestamp int64 `json:"timestamp"`
Servers []PrivatevpnServer `json:"servers"`
}
type ProtonvpnServers struct {
Version uint16 `json:"version"`
Timestamp int64 `json:"timestamp"`
Servers []ProtonvpnServer `json:"servers"`
}
type PurevpnServers struct {
Version uint16 `json:"version"`
Timestamp int64 `json:"timestamp"`

View File

@@ -0,0 +1,214 @@
package provider
import (
"context"
"fmt"
"math/rand"
"net"
"net/http"
"strconv"
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
type protonvpn struct {
servers []models.ProtonvpnServer
randSource rand.Source
}
func newProtonvpn(servers []models.ProtonvpnServer, timeNow timeNowFunc) *protonvpn {
return &protonvpn{
servers: servers,
randSource: rand.NewSource(timeNow().UnixNano()),
}
}
func (p *protonvpn) GetOpenVPNConnection(selection configuration.ServerSelection) (
connection models.OpenVPNConnection, err error) {
port, err := p.getPort(selection)
if err != nil {
return connection, err
}
if selection.TargetIP != nil {
return models.OpenVPNConnection{
IP: selection.TargetIP,
Port: port,
Protocol: selection.Protocol,
}, nil
}
servers := p.filterServers(selection.Countries, selection.Regions,
selection.Cities, selection.Names, selection.Hostnames)
if len(servers) == 0 {
return connection, p.notFoundErr(selection)
}
connections := make([]models.OpenVPNConnection, len(servers))
for i := range servers {
connections[i] = models.OpenVPNConnection{
IP: servers[i].EntryIP,
Port: port,
Protocol: selection.Protocol,
}
}
return pickRandomConnection(connections, p.randSource), nil
}
func (p *protonvpn) BuildConf(connection models.OpenVPNConnection,
username string, settings configuration.OpenVPN) (lines []string) {
if len(settings.Cipher) == 0 {
settings.Cipher = aes256cbc
}
if len(settings.Auth) == 0 {
settings.Auth = "SHA512"
}
const defaultMSSFix = 1450
if settings.MSSFix == 0 {
settings.MSSFix = defaultMSSFix
}
lines = []string{
"client",
"dev tun",
"nobind",
"persist-key",
"remote-cert-tls server",
"tls-exit",
// Protonvpn specific
"tun-mtu 1500",
"tun-mtu-extra 32",
"mssfix " + strconv.Itoa(int(settings.MSSFix)),
"reneg-sec 0",
"fast-io",
"key-direction 1",
"pull",
"comp-lzo no",
// Added constant values
"auth-nocache",
"mute-replay-warnings",
"pull-filter ignore \"auth-token\"", // prevent auth failed loops
"pull-filter ignore \"block-outside-dns\"",
`pull-filter ignore "ping-restart"`,
"auth-retry nointeract",
"suppress-timestamps",
// Modified variables
"verb " + strconv.Itoa(settings.Verbosity),
"auth-user-pass " + constants.OpenVPNAuthConf,
"proto " + connection.Protocol,
"remote " + connection.IP.String() + " " + strconv.Itoa(int(connection.Port)),
"cipher " + settings.Cipher,
"auth " + settings.Auth,
}
if !settings.Root {
lines = append(lines, "user "+username)
}
lines = append(lines, []string{
"<ca>",
"-----BEGIN CERTIFICATE-----",
constants.ProtonvpnCertificate,
"-----END CERTIFICATE-----",
"</ca>",
}...)
lines = append(lines, []string{
"<tls-auth>",
"-----BEGIN OpenVPN Static key V1-----",
constants.ProtonvpnOpenvpnStaticKeyV1,
"-----END OpenVPN Static key V1-----",
"</tls-auth>",
"",
}...)
return lines
}
func (p *protonvpn) PortForward(ctx context.Context, client *http.Client,
openFile os.OpenFileFunc, pfLogger logging.Logger, gateway net.IP, fw firewall.Configurator,
syncState func(port uint16) (pfFilepath string)) {
panic("port forwarding is not supported for protonvpn")
}
func (p *protonvpn) getPort(selection configuration.ServerSelection) (port uint16, err error) {
if selection.CustomPort == 0 {
switch selection.Protocol {
case constants.TCP:
const defaultTCPPort = 443
return defaultTCPPort, nil
case constants.UDP:
const defaultUDPPort = 1194
return defaultUDPPort, nil
}
}
port = selection.CustomPort
switch selection.Protocol {
case constants.TCP:
switch port {
case 443, 5995, 8443: //nolint:gomnd
default:
return 0, fmt.Errorf("%w: %d for protocol %s",
ErrInvalidPort, port, selection.Protocol)
}
case constants.UDP:
switch port {
case 80, 443, 1194, 4569, 5060: //nolint:gomnd
default:
return 0, fmt.Errorf("%w: %d for protocol %s",
ErrInvalidPort, port, selection.Protocol)
}
}
return port, nil
}
func (p *protonvpn) filterServers(countries, regions, cities, names, hostnames []string) (
servers []models.ProtonvpnServer) {
for _, server := range p.servers {
switch {
case
filterByPossibilities(server.Country, countries),
filterByPossibilities(server.Region, regions),
filterByPossibilities(server.City, cities),
filterByPossibilities(server.Name, names),
filterByPossibilities(server.Hostname, hostnames):
default:
servers = append(servers, server)
}
}
return servers
}
func (p *protonvpn) notFoundErr(selection configuration.ServerSelection) error {
message := "no server found for protocol " + selection.Protocol
if len(selection.Countries) > 0 {
message += " + countries " + commaJoin(selection.Countries)
}
if len(selection.Regions) > 0 {
message += " + regions " + commaJoin(selection.Regions)
}
if len(selection.Cities) > 0 {
message += " + cities " + commaJoin(selection.Cities)
}
if len(selection.Names) > 0 {
message += " + names " + commaJoin(selection.Names)
}
if len(selection.Hostnames) > 0 {
message += " + hostnames " + commaJoin(selection.Hostnames)
}
return fmt.Errorf(message)
}

View File

@@ -41,6 +41,8 @@ func New(provider string, allServers models.AllServers, timeNow timeNowFunc) Pro
return newPrivateInternetAccess(allServers.Pia.Servers, timeNow)
case constants.Privatevpn:
return newPrivatevpn(allServers.Privatevpn.Servers, timeNow)
case constants.Protonvpn:
return newProtonvpn(allServers.Protonvpn.Servers, timeNow)
case constants.Purevpn:
return newPurevpn(allServers.Purevpn.Servers, timeNow)
case constants.Surfshark:

View File

@@ -25,6 +25,7 @@ func (s *storage) mergeServers(hardcoded, persisted models.AllServers) models.Al
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),
@@ -140,6 +141,22 @@ func (s *storage) mergePrivatevpn(hardcoded, persisted models.PrivatevpnServers)
return persisted
}
func (s *storage) mergeProtonvpn(hardcoded, persisted models.ProtonvpnServers) models.ProtonvpnServers {
if persisted.Timestamp <= hardcoded.Timestamp {
return hardcoded
}
versionDiff := hardcoded.Version - persisted.Version
if versionDiff > 0 {
s.logger.Info(
"Protonvpn servers from file discarded because they are %d versions behind",
versionDiff)
return hardcoded
}
s.logger.Info("Using Protonvpn servers from file (%s more recent)",
getUnixTimeDifference(persisted.Timestamp, hardcoded.Timestamp))
return persisted
}
func (s *storage) mergePureVPN(hardcoded, persisted models.PurevpnServers) models.PurevpnServers {
if persisted.Timestamp <= hardcoded.Timestamp {
return hardcoded

View File

@@ -25,6 +25,7 @@ func countServers(allServers models.AllServers) int {
len(allServers.Privado.Servers) +
len(allServers.Pia.Servers) +
len(allServers.Privatevpn.Servers) +
len(allServers.Protonvpn.Servers) +
len(allServers.Purevpn.Servers) +
len(allServers.Surfshark.Servers) +
len(allServers.Torguard.Servers) +

View File

@@ -0,0 +1,141 @@
package updater
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"sort"
"strings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models"
)
func (u *updater) updateProtonvpn(ctx context.Context) (err error) {
servers, warnings, err := findProtonvpnServers(ctx, u.client)
if u.options.CLI {
for _, warning := range warnings {
u.logger.Warn("Protonvpn: %s", warning)
}
}
if err != nil {
return fmt.Errorf("cannot update Protonvpn servers: %w", err)
}
if u.options.Stdout {
u.println(stringifyProtonvpnServers(servers))
}
u.servers.Protonvpn.Timestamp = u.timeNow().Unix()
u.servers.Protonvpn.Servers = servers
return nil
}
func findProtonvpnServers(ctx context.Context, client *http.Client) (
servers []models.ProtonvpnServer, warnings []string, err error) {
const url = "https://api.protonmail.ch/vpn/logicals"
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, nil, err
}
response, err := client.Do(request)
if err != nil {
return nil, nil, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, nil, fmt.Errorf("%w: %s for %s", ErrHTTPStatusCodeNotOK, response.Status, url)
}
decoder := json.NewDecoder(response.Body)
var data struct {
LogicalServers []struct {
Name string
ExitCountry string
Region *string
City *string
Servers []struct {
EntryIP net.IP
ExitIP net.IP
Domain string
Status uint8
}
}
}
if err := decoder.Decode(&data); err != nil {
return nil, nil, err
}
if err := response.Body.Close(); err != nil {
return nil, nil, err
}
countryCodesMapping := constants.CountryCodes()
for _, logicalServer := range data.LogicalServers {
for _, physicalServer := range logicalServer.Servers {
if physicalServer.Status == 0 {
warnings = append(warnings, "ignoring server "+physicalServer.Domain+" as its status is 0")
continue
}
countryCode := strings.ToLower(logicalServer.ExitCountry)
country, ok := countryCodesMapping[countryCode]
if !ok {
warnings = append(warnings, "country not found for country code "+countryCode)
country = logicalServer.ExitCountry
}
server := models.ProtonvpnServer{
// Note: for multi-hop use the server name or hostname instead of the country
Country: country,
Region: getStringValue(logicalServer.Region),
City: getStringValue(logicalServer.City),
Name: logicalServer.Name,
Hostname: physicalServer.Domain,
EntryIP: physicalServer.EntryIP,
ExitIP: physicalServer.ExitIP,
}
servers = append(servers, server)
}
}
sort.Slice(servers, func(i, j int) bool {
a, b := servers[i], servers[j]
if a.Country == b.Country { //nolint:nestif
if a.Region == b.Region {
if a.City == b.City {
if a.Name == b.Name {
return a.Hostname < b.Hostname
}
return a.Name < b.Name
}
return a.City < b.City
}
return a.Region < b.Region
}
return a.Country < b.Country
})
return servers, warnings, nil
}
func getStringValue(ptr *string) string {
if ptr == nil {
return ""
}
return *ptr
}
func stringifyProtonvpnServers(servers []models.ProtonvpnServer) (s string) {
s = "func ProtonvpnServers() []models.ProtonvpnServer {\n"
s += " return []models.ProtonvpnServer{\n"
for _, server := range servers {
s += " " + server.String() + ",\n"
}
s += " }\n"
s += "}"
return s
}

View File

@@ -131,6 +131,16 @@ func (u *updater) UpdateServers(ctx context.Context) (allServers models.AllServe
}
}
if u.options.Protonvpn {
u.logger.Info("updating Protonvpn servers...")
if err := u.updateProtonvpn(ctx); err != nil {
if ctxErr := ctx.Err(); ctxErr != nil {
return allServers, ctxErr
}
u.logger.Error(err)
}
}
if u.options.Purevpn {
u.logger.Info("updating PureVPN servers...")
// TODO support servers offering only TCP or only UDP