2
.github/labels.yml
vendored
2
.github/labels.yml
vendored
@@ -39,6 +39,8 @@
|
||||
- name: ":cloud: PrivateVPN"
|
||||
color: "cfe8d4"
|
||||
description: ""
|
||||
- name: ":cloud: ProtonVPN"
|
||||
color: "cfe8d4"
|
||||
- name: ":cloud: PureVPN"
|
||||
color: "cfe8d4"
|
||||
description: ""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
75
internal/configuration/protonvpn.go
Normal file
75
internal/configuration/protonvpn.go
Normal 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
|
||||
}
|
||||
@@ -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":
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
|
||||
1635
internal/constants/protonvpn.go
Normal file
1635
internal/constants/protonvpn.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"`
|
||||
|
||||
214
internal/provider/protonvpn.go
Normal file
214
internal/provider/protonvpn.go
Normal 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)
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) +
|
||||
|
||||
141
internal/updater/protonvpn.go
Normal file
141
internal/updater/protonvpn.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user