Servers updater (#232)

* Support for all VPN providers
* Update all VPN providers servers information
* Remove old tooling binaries
This commit is contained in:
Quentin McGaw
2020-09-05 12:57:16 -04:00
committed by GitHub
parent 9dcc00900e
commit 797fa33971
30 changed files with 4028 additions and 3818 deletions

12
internal/updater/alias.go Normal file
View File

@@ -0,0 +1,12 @@
package updater
import (
"context"
"net"
"net/http"
)
type (
httpGetFunc func(url string) (r *http.Response, err error)
lookupIPFunc func(ctx context.Context, host string) (ips []net.IP, err error)
)

View File

@@ -0,0 +1,254 @@
package updater
func getCountryCodes() map[string]string { //nolint:dupl
return map[string]string{
"af": "Afghanistan",
"ax": "Aland Islands",
"al": "Albania",
"dz": "Algeria",
"as": "American Samoa",
"ad": "Andorra",
"ao": "Angola",
"ai": "Anguilla",
"aq": "Antarctica",
"ag": "Antigua and Barbuda",
"ar": "Argentina",
"am": "Armenia",
"aw": "Aruba",
"au": "Australia",
"at": "Austria",
"az": "Azerbaijan",
"bs": "Bahamas",
"bh": "Bahrain",
"bd": "Bangladesh",
"bb": "Barbados",
"by": "Belarus",
"be": "Belgium",
"bz": "Belize",
"bj": "Benin",
"bm": "Bermuda",
"bt": "Bhutan",
"bo": "Bolivia",
"bq": "Bonaire",
"ba": "Bosnia and Herzegovina",
"bw": "Botswana",
"bv": "Bouvet Island",
"br": "Brazil",
"io": "British Indian Ocean Territory",
"vg": "British Virgin Islands",
"bn": "Brunei Darussalam",
"bg": "Bulgaria",
"bf": "Burkina Faso",
"bi": "Burundi",
"kh": "Cambodia",
"cm": "Cameroon",
"ca": "Canada",
"cv": "Cape Verde",
"ky": "Cayman Islands",
"cf": "Central African Republic",
"td": "Chad",
"cl": "Chile",
"cn": "China",
"cx": "Christmas Island",
"cc": "Cocos Islands",
"co": "Colombia",
"km": "Comoros",
"cg": "Congo",
"ck": "Cook Islands",
"cr": "Costa Rica",
"ci": "Cote d'Ivoire",
"hr": "Croatia",
"cu": "Cuba",
"cw": "Curacao",
"cy": "Cyprus",
"cz": "Czech Republic",
"cd": "Democratic Republic of the Congo",
"dk": "Denmark",
"dj": "Djibouti",
"dm": "Dominica",
"do": "Dominican Republic",
"ec": "Ecuador",
"eg": "Egypt",
"sv": "El Salvador",
"gq": "Equatorial Guinea",
"er": "Eritrea",
"ee": "Estonia",
"et": "Ethiopia",
"fk": "Falkland Islands",
"fo": "Faroe Islands",
"fj": "Fiji",
"fi": "Finland",
"fr": "France",
"gf": "French Guiana",
"pf": "French Polynesia",
"tf": "French Southern Territories",
"ga": "Gabon",
"gm": "Gambia",
"ge": "Georgia",
"de": "Germany",
"gh": "Ghana",
"gi": "Gibraltar",
"gr": "Greece",
"gl": "Greenland",
"gd": "Grenada",
"gp": "Guadeloupe",
"gu": "Guam",
"gt": "Guatemala",
"gg": "Guernsey",
"gw": "Guinea-Bissau",
"gn": "Guinea",
"gy": "Guyana",
"ht": "Haiti",
"hm": "Heard Island and McDonald Islands",
"hn": "Honduras",
"hk": "Hong Kong",
"hu": "Hungary",
"is": "Iceland",
"in": "India",
"id": "Indonesia",
"ir": "Iran",
"iq": "Iraq",
"ie": "Ireland",
"im": "Isle of Man",
"il": "Israel",
"it": "Italy",
"jm": "Jamaica",
"jp": "Japan",
"je": "Jersey",
"jo": "Jordan",
"kz": "Kazakhstan",
"ke": "Kenya",
"ki": "Kiribati",
"kr": "Korea",
"kw": "Kuwait",
"kg": "Kyrgyzstan",
"la": "Lao People's Democratic Republic",
"lv": "Latvia",
"lb": "Lebanon",
"ls": "Lesotho",
"lr": "Liberia",
"ly": "Libya",
"li": "Liechtenstein",
"lt": "Lithuania",
"lu": "Luxembourg",
"mo": "Macao",
"mk": "Macedonia",
"mg": "Madagascar",
"mw": "Malawi",
"my": "Malaysia",
"mv": "Maldives",
"ml": "Mali",
"mt": "Malta",
"mh": "Marshall Islands",
"mq": "Martinique",
"mr": "Mauritania",
"mu": "Mauritius",
"yt": "Mayotte",
"mx": "Mexico",
"fm": "Micronesia",
"md": "Moldova",
"mc": "Monaco",
"mn": "Mongolia",
"me": "Montenegro",
"ms": "Montserrat",
"ma": "Morocco",
"mz": "Mozambique",
"mm": "Myanmar",
"na": "Namibia",
"nr": "Nauru",
"np": "Nepal",
"nl": "Netherlands",
"nc": "New Caledonia",
"nz": "New Zealand",
"ni": "Nicaragua",
"ne": "Niger",
"ng": "Nigeria",
"nu": "Niue",
"nf": "Norfolk Island",
"mp": "Northern Mariana Islands",
"no": "Norway",
"om": "Oman",
"pk": "Pakistan",
"pw": "Palau",
"ps": "Palestine, State of",
"pa": "Panama",
"pg": "Papua New Guinea",
"py": "Paraguay",
"pe": "Peru",
"ph": "Philippines",
"pn": "Pitcairn",
"pl": "Poland",
"pt": "Portugal",
"pr": "Puerto Rico",
"qa": "Qatar",
"re": "Reunion",
"ro": "Romania",
"ru": "Russian Federation",
"rw": "Rwanda",
"bl": "Saint Barthelemy",
"sh": "Saint Helena",
"kn": "Saint Kitts and Nevis",
"lc": "Saint Lucia",
"mf": "Saint Martin",
"pm": "Saint Pierre and Miquelon",
"vc": "Saint Vincent and the Grenadines",
"ws": "Samoa",
"sm": "San Marino",
"st": "Sao Tome and Principe",
"sa": "Saudi Arabia",
"sn": "Senegal",
"rs": "Serbia",
"sc": "Seychelles",
"sl": "Sierra Leone",
"sg": "Singapore",
"sx": "Sint Maarten",
"sk": "Slovakia",
"si": "Slovenia",
"sb": "Solomon Islands",
"so": "Somalia",
"za": "South Africa",
"gs": "South Georgia and the South Sandwich Islands",
"ss": "South Sudan",
"es": "Spain",
"lk": "Sri Lanka",
"sd": "Sudan",
"sr": "Suriname",
"sj": "Svalbard and Jan Mayen",
"sz": "Swaziland",
"se": "Sweden",
"ch": "Switzerland",
"sy": "Syrian Arab Republic",
"tw": "Taiwan",
"tj": "Tajikistan",
"tz": "Tanzania",
"th": "Thailand",
"tl": "Timor-Leste",
"tg": "Togo",
"tk": "Tokelau",
"to": "Tonga",
"tt": "Trinidad and Tobago",
"tn": "Tunisia",
"tr": "Turkey",
"tm": "Turkmenistan",
"tc": "Turks and Caicos Islands",
"tv": "Tuvalu",
"ug": "Uganda",
"ua": "Ukraine",
"ae": "United Arab Emirates",
"gb": "United Kingdom",
"um": "United States Minor Outlying Islands",
"us": "United States",
"uy": "Uruguay",
"vi": "US Virgin Islands",
"uz": "Uzbekistan",
"vu": "Vanuatu",
"va": "Vatican City State",
"ve": "Venezuela",
"vn": "Vietnam",
"wf": "Wallis and Futuna",
"eh": "Western Sahara",
"ye": "Yemen",
"zm": "Zambia",
"zw": "Zimbabwe",
}
}

View File

@@ -0,0 +1,336 @@
package updater
import (
"context"
"fmt"
"sort"
"github.com/qdm12/gluetun/internal/models"
)
func (u *updater) updateCyberghost(ctx context.Context) {
servers := findCyberghostServers(ctx, u.lookupIP)
if u.options.Stdout {
u.println(stringifyCyberghostServers(servers))
}
u.servers.Cyberghost.Timestamp = u.timeNow().Unix()
u.servers.Cyberghost.Servers = servers
}
func findCyberghostServers(ctx context.Context, lookupIP lookupIPFunc) (servers []models.CyberghostServer) {
groups := getCyberghostGroups()
allCountryCodes := getCountryCodes()
cyberghostCountryCodes := getCyberghostSubdomainToRegion()
possibleCountryCodes := mergeCountryCodes(cyberghostCountryCodes, allCountryCodes)
resultsChannel := make(chan models.CyberghostServer)
const maxGoroutines = 10
guard := make(chan struct{}, maxGoroutines)
for groupID, groupName := range groups {
for countryCode, region := range possibleCountryCodes {
go func(groupName, groupID, region, countryCode string) {
host := fmt.Sprintf("%s-%s.cg-dialup.net", groupID, countryCode)
guard <- struct{}{}
IPs, err := resolveRepeat(ctx, lookupIP, host, 2)
if err != nil {
IPs = nil
}
<-guard
resultsChannel <- models.CyberghostServer{
Region: region,
Group: groupName,
IPs: IPs,
}
}(groupName, groupID, region, countryCode)
}
}
for i := 0; i < len(groups)*len(possibleCountryCodes); i++ {
server := <-resultsChannel
if server.IPs == nil {
continue
}
servers = append(servers, server)
}
sort.Slice(servers, func(i, j int) bool {
return servers[i].Region < servers[j].Region
})
return servers
}
//nolint:goconst
func stringifyCyberghostServers(servers []models.CyberghostServer) (s string) {
s = "func CyberghostServers() []models.CyberghostServer {\n"
s += " return []models.CyberghostServer{\n"
for _, server := range servers {
s += " " + server.String() + ",\n"
}
s += " }\n"
s += "}"
return s
}
func getCyberghostGroups() map[string]string {
return map[string]string{
"87-1": "Premium UDP Europe",
"94-1": "Premium UDP USA",
"95-1": "Premium UDP Asia",
"87-8": "NoSpy UDP Europe",
"97-1": "Premium TCP Europe",
"93-1": "Premium TCP USA",
"96-1": "Premium TCP Asia",
"97-8": "NoSpy TCP Europe",
}
}
func getCyberghostSubdomainToRegion() map[string]string { //nolint:dupl
return map[string]string{
"af": "Afghanistan",
"ax": "Aland Islands",
"al": "Albania",
"dz": "Algeria",
"as": "American Samoa",
"ad": "Andorra",
"ao": "Angola",
"ai": "Anguilla",
"aq": "Antarctica",
"ag": "Antigua and Barbuda",
"ar": "Argentina",
"am": "Armenia",
"aw": "Aruba",
"au": "Australia",
"at": "Austria",
"az": "Azerbaijan",
"bs": "Bahamas",
"bh": "Bahrain",
"bd": "Bangladesh",
"bb": "Barbados",
"by": "Belarus",
"be": "Belgium",
"bz": "Belize",
"bj": "Benin",
"bm": "Bermuda",
"bt": "Bhutan",
"bo": "Bolivia",
"bq": "Bonaire",
"ba": "Bosnia and Herzegovina",
"bw": "Botswana",
"bv": "Bouvet Island",
"br": "Brazil",
"io": "British Indian Ocean Territory",
"vg": "British Virgin Islands",
"bn": "Brunei Darussalam",
"bg": "Bulgaria",
"bf": "Burkina Faso",
"bi": "Burundi",
"kh": "Cambodia",
"cm": "Cameroon",
"ca": "Canada",
"cv": "Cape Verde",
"ky": "Cayman Islands",
"cf": "Central African Republic",
"td": "Chad",
"cl": "Chile",
"cn": "China",
"cx": "Christmas Island",
"cc": "Cocos Islands",
"co": "Colombia",
"km": "Comoros",
"cg": "Congo",
"ck": "Cook Islands",
"cr": "Costa Rica",
"ci": "Cote d'Ivoire",
"hr": "Croatia",
"cu": "Cuba",
"cw": "Curacao",
"cy": "Cyprus",
"cz": "Czech Republic",
"cd": "Democratic Republic of the Congo",
"dk": "Denmark",
"dj": "Djibouti",
"dm": "Dominica",
"do": "Dominican Republic",
"ec": "Ecuador",
"eg": "Egypt",
"sv": "El Salvador",
"gq": "Equatorial Guinea",
"er": "Eritrea",
"ee": "Estonia",
"et": "Ethiopia",
"fk": "Falkland Islands",
"fo": "Faroe Islands",
"fj": "Fiji",
"fi": "Finland",
"fr": "France",
"gf": "French Guiana",
"pf": "French Polynesia",
"tf": "French Southern Territories",
"ga": "Gabon",
"gm": "Gambia",
"ge": "Georgia",
"de": "Germany",
"gh": "Ghana",
"gi": "Gibraltar",
"gr": "Greece",
"gl": "Greenland",
"gd": "Grenada",
"gp": "Guadeloupe",
"gu": "Guam",
"gt": "Guatemala",
"gg": "Guernsey",
"gw": "Guinea-Bissau",
"gn": "Guinea",
"gy": "Guyana",
"ht": "Haiti",
"hm": "Heard Island and McDonald Islands",
"hn": "Honduras",
"hk": "Hong Kong",
"hu": "Hungary",
"is": "Iceland",
"in": "India",
"id": "Indonesia",
"ir": "Iran",
"iq": "Iraq",
"ie": "Ireland",
"im": "Isle of Man",
"il": "Israel",
"it": "Italy",
"jm": "Jamaica",
"jp": "Japan",
"je": "Jersey",
"jo": "Jordan",
"kz": "Kazakhstan",
"ke": "Kenya",
"ki": "Kiribati",
"kr": "Korea",
"kw": "Kuwait",
"kg": "Kyrgyzstan",
"la": "Lao People's Democratic Republic",
"lv": "Latvia",
"lb": "Lebanon",
"ls": "Lesotho",
"lr": "Liberia",
"ly": "Libya",
"li": "Liechtenstein",
"lt": "Lithuania",
"lu": "Luxembourg",
"mo": "Macao",
"mk": "Macedonia",
"mg": "Madagascar",
"mw": "Malawi",
"my": "Malaysia",
"mv": "Maldives",
"ml": "Mali",
"mt": "Malta",
"mh": "Marshall Islands",
"mq": "Martinique",
"mr": "Mauritania",
"mu": "Mauritius",
"yt": "Mayotte",
"mx": "Mexico",
"fm": "Micronesia",
"md": "Moldova",
"mc": "Monaco",
"mn": "Mongolia",
"me": "Montenegro",
"ms": "Montserrat",
"ma": "Morocco",
"mz": "Mozambique",
"mm": "Myanmar",
"na": "Namibia",
"nr": "Nauru",
"np": "Nepal",
"nl": "Netherlands",
"nc": "New Caledonia",
"nz": "New Zealand",
"ni": "Nicaragua",
"ne": "Niger",
"ng": "Nigeria",
"nu": "Niue",
"nf": "Norfolk Island",
"mp": "Northern Mariana Islands",
"no": "Norway",
"om": "Oman",
"pk": "Pakistan",
"pw": "Palau",
"ps": "Palestine, State of",
"pa": "Panama",
"pg": "Papua New Guinea",
"py": "Paraguay",
"pe": "Peru",
"ph": "Philippines",
"pn": "Pitcairn",
"pl": "Poland",
"pt": "Portugal",
"pr": "Puerto Rico",
"qa": "Qatar",
"re": "Reunion",
"ro": "Romania",
"ru": "Russian Federation",
"rw": "Rwanda",
"bl": "Saint Barthelemy",
"sh": "Saint Helena",
"kn": "Saint Kitts and Nevis",
"lc": "Saint Lucia",
"mf": "Saint Martin",
"pm": "Saint Pierre and Miquelon",
"vc": "Saint Vincent and the Grenadines",
"ws": "Samoa",
"sm": "San Marino",
"st": "Sao Tome and Principe",
"sa": "Saudi Arabia",
"sn": "Senegal",
"rs": "Serbia",
"sc": "Seychelles",
"sl": "Sierra Leone",
"sg": "Singapore",
"sx": "Sint Maarten",
"sk": "Slovakia",
"si": "Slovenia",
"sb": "Solomon Islands",
"so": "Somalia",
"za": "South Africa",
"gs": "South Georgia and the South Sandwich Islands",
"ss": "South Sudan",
"es": "Spain",
"lk": "Sri Lanka",
"sd": "Sudan",
"sr": "Suriname",
"sj": "Svalbard and Jan Mayen",
"sz": "Swaziland",
"se": "Sweden",
"ch": "Switzerland",
"sy": "Syrian Arab Republic",
"tw": "Taiwan",
"tj": "Tajikistan",
"tz": "Tanzania",
"th": "Thailand",
"tl": "Timor-Leste",
"tg": "Togo",
"tk": "Tokelau",
"to": "Tonga",
"tt": "Trinidad and Tobago",
"tn": "Tunisia",
"tr": "Turkey",
"tm": "Turkmenistan",
"tc": "Turks and Caicos Islands",
"tv": "Tuvalu",
"ug": "Uganda",
"ua": "Ukraine",
"ae": "United Arab Emirates",
"gb": "United Kingdom",
"um": "United States Minor Outlying Islands",
"us": "United States",
"uy": "Uruguay",
"vi": "US Virgin Islands",
"uz": "Uzbekistan",
"vu": "Vanuatu",
"va": "Vatican City State",
"ve": "Venezuela",
"vn": "Vietnam",
"wf": "Wallis and Futuna",
"eh": "Western Sahara",
"ye": "Yemen",
"zm": "Zambia",
"zw": "Zimbabwe",
}
}

24
internal/updater/ips.go Normal file
View File

@@ -0,0 +1,24 @@
package updater
import (
"bytes"
"net"
"sort"
)
func uniqueSortedIPs(ips []net.IP) []net.IP {
uniqueIPs := make(map[string]struct{})
for _, ip := range ips {
uniqueIPs[ip.String()] = struct{}{}
}
ips = make([]net.IP, len(uniqueIPs))
i := 0
for ip := range uniqueIPs {
ips[i] = net.ParseIP(ip)
i++
}
sort.Slice(ips, func(i, j int) bool {
return bytes.Compare(ips[i], ips[j]) < 0
})
return ips
}

View File

@@ -11,9 +11,22 @@ import (
"github.com/qdm12/gluetun/internal/models"
)
func (u *updater) findMullvadServers() (servers []models.MullvadServer, err error) {
func (u *updater) updateMullvad() (err error) {
servers, err := findMullvadServers(u.httpGet)
if err != nil {
return fmt.Errorf("cannot update Mullvad servers: %w", err)
}
if u.options.Stdout {
u.println(stringifyMullvadServers(servers))
}
u.servers.Mullvad.Timestamp = u.timeNow().Unix()
u.servers.Mullvad.Servers = servers
return nil
}
func findMullvadServers(httpGet httpGetFunc) (servers []models.MullvadServer, err error) {
const url = "https://api.mullvad.net/www/relays/openvpn/"
response, err := u.httpGet(url)
response, err := httpGet(url)
if err != nil {
return nil, err
}
@@ -66,6 +79,8 @@ func (u *updater) findMullvadServers() (servers []models.MullvadServer, err erro
}
}
for _, server := range serversByKey {
server.IPs = uniqueSortedIPs(server.IPs)
server.IPsV6 = uniqueSortedIPs(server.IPsV6)
servers = append(servers, server)
}
sort.Slice(servers, func(i, j int) bool {

105
internal/updater/nordvpn.go Normal file
View File

@@ -0,0 +1,105 @@
package updater
import (
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"sort"
"strconv"
"strings"
"github.com/qdm12/gluetun/internal/models"
)
func (u *updater) updateNordvpn() (err error) {
servers, warnings, err := findNordvpnServers(u.httpGet)
for _, warning := range warnings {
u.println(warning)
}
if err != nil {
return fmt.Errorf("cannot update Nordvpn servers: %w", err)
}
if u.options.Stdout {
u.println(stringifyNordvpnServers(servers))
}
u.servers.Nordvpn.Timestamp = u.timeNow().Unix()
u.servers.Nordvpn.Servers = servers
return nil
}
func findNordvpnServers(httpGet httpGetFunc) (servers []models.NordvpnServer, warnings []string, err error) {
const url = "https://nordvpn.com/api/server"
response, err := httpGet(url)
if err != nil {
return nil, nil, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, nil, fmt.Errorf(response.Status)
}
bytes, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, nil, err
}
var data []struct {
IPAddress string `json:"ip_address"`
Name string `json:"name"`
Country string `json:"country"`
Features struct {
UDP bool `json:"openvpn_udp"`
TCP bool `json:"openvpn_tcp"`
} `json:"features"`
}
if err := json.Unmarshal(bytes, &data); err != nil {
return nil, nil, err
}
sort.Slice(data, func(i, j int) bool {
if data[i].Country == data[j].Country {
return data[i].Name < data[j].Name
}
return data[i].Country < data[j].Country
})
for _, jsonServer := range data {
if !jsonServer.Features.TCP && !jsonServer.Features.UDP {
warnings = append(warnings, fmt.Sprintf("server %q does not support TCP and UDP for openvpn", jsonServer.Name))
continue
}
ip := net.ParseIP(jsonServer.IPAddress)
if ip == nil || ip.To4() == nil {
return nil, nil, fmt.Errorf("IP address %q is not a valid IPv4 address for server %q", jsonServer.IPAddress, jsonServer.Name)
}
i := strings.IndexRune(jsonServer.Name, '#')
if i < 0 {
return nil, nil, fmt.Errorf("No ID in server name %q", jsonServer.Name)
}
idString := jsonServer.Name[i+1:]
idUint64, err := strconv.ParseUint(idString, 10, 16)
if err != nil {
return nil, nil, fmt.Errorf("Bad ID in server name %q", jsonServer.Name)
}
server := models.NordvpnServer{
Region: jsonServer.Country,
Number: uint16(idUint64),
IP: ip,
TCP: jsonServer.Features.TCP,
UDP: jsonServer.Features.UDP,
}
servers = append(servers, server)
}
return servers, warnings, nil
}
//nolint:goconst
func stringifyNordvpnServers(servers []models.NordvpnServer) (s string) {
s = "func NordvpnServers() []models.NordvpnServer {\n"
s += " return []models.NordvpnServer{\n"
for _, server := range servers {
s += " " + server.String() + ",\n"
}
s += " }\n"
s += "}"
return s
}

View File

@@ -26,3 +26,14 @@ func extractIPsFromRemoteLines(remoteLines []string) (ips []net.IP) {
}
return ips
}
func extractHostnamesFromRemoteLines(remoteLines []string) (hostnames []string) {
for _, remoteLine := range remoteLines {
fields := strings.Fields(remoteLine)
if len(fields[1]) == 0 {
continue
}
hostnames = append(hostnames, fields[1])
}
return hostnames
}

View File

@@ -1,9 +1,16 @@
package updater
type Options struct {
PIA bool
PIAold bool
Mullvad bool
File bool // update JSON file (user side)
Stdout bool // update constants file (maintainer side)
Cyberghost bool
Mullvad bool
Nordvpn bool
PIA bool
PIAold bool
Purevpn bool
Surfshark bool
Vyprvpn bool
Windscribe bool
File bool // update JSON file (user side)
Stdout bool // update constants file (maintainer side)
DNSAddress string
}

View File

@@ -8,12 +8,32 @@ import (
"github.com/qdm12/gluetun/internal/models"
)
func findPIAServers(new bool) (servers []models.PIAServer, err error) {
zipURL := "https://www.privateinternetaccess.com/openvpn/openvpn-ip.zip"
if new {
zipURL = "https://www.privateinternetaccess.com/openvpn/openvpn-ip-nextgen.zip"
func (u *updater) updatePIA() (err error) {
const zipURL = "https://www.privateinternetaccess.com/openvpn/openvpn-ip-nextgen.zip"
servers, err := findPIAServersFromURL(zipURL)
if err != nil {
return fmt.Errorf("cannot update PIA servers: %w", err)
}
return findPIAServersFromURL(zipURL)
if u.options.Stdout {
u.println(stringifyPIAServers(servers))
}
u.servers.Pia.Timestamp = u.timeNow().Unix()
u.servers.Pia.Servers = servers
return nil
}
func (u *updater) updatePIAOld() (err error) {
const zipURL = "https://www.privateinternetaccess.com/openvpn/openvpn-ip.zip"
servers, err := findPIAServersFromURL(zipURL)
if err != nil {
return fmt.Errorf("cannot update old PIA servers: %w", err)
}
if u.options.Stdout {
u.println(stringifyPIAOldServers(servers))
}
u.servers.PiaOld.Timestamp = u.timeNow().Unix()
u.servers.PiaOld.Servers = servers
return nil
}
func findPIAServersFromURL(zipURL string) (servers []models.PIAServer, err error) {
@@ -33,7 +53,7 @@ func findPIAServersFromURL(zipURL string) (servers []models.PIAServer, err error
region := strings.TrimSuffix(fileName, ".ovpn")
server := models.PIAServer{
Region: region,
IPs: IPs,
IPs: uniqueSortedIPs(IPs),
}
servers = append(servers, server)
}

113
internal/updater/purevpn.go Normal file
View File

@@ -0,0 +1,113 @@
package updater
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"sort"
"strings"
"github.com/qdm12/gluetun/internal/models"
)
func (u *updater) updatePurevpn(ctx context.Context) (err error) {
servers, warnings, err := findPurevpnServers(ctx, u.httpGet, u.lookupIP)
for _, warning := range warnings {
u.println(warning)
}
if err != nil {
return fmt.Errorf("cannot update Purevpn servers: %w", err)
}
if u.options.Stdout {
u.println(stringifyPurevpnServers(servers))
}
u.servers.Purevpn.Timestamp = u.timeNow().Unix()
u.servers.Purevpn.Servers = servers
return nil
}
func findPurevpnServers(ctx context.Context, httpGet httpGetFunc, lookupIP lookupIPFunc) (
servers []models.PurevpnServer, warnings []string, err error) {
const url = "https://support.purevpn.com/vpn-servers"
response, err := httpGet(url)
if err != nil {
return nil, nil, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, nil, fmt.Errorf(response.Status)
}
bytes, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, nil, err
}
const jsonPrefix = "<script>var servers = "
const jsonSuffix = "</script>"
s := string(bytes)
jsonPrefixIndex := strings.Index(s, jsonPrefix)
if jsonPrefixIndex == -1 {
return nil, nil, fmt.Errorf("cannot find %q in html", jsonPrefix)
}
s = s[jsonPrefixIndex+len(jsonPrefix):]
endIndex := strings.Index(s, jsonSuffix)
if endIndex == -1 {
return nil, nil, fmt.Errorf("cannot find %q after %q in html", jsonSuffix, jsonPrefix)
}
s = s[:endIndex]
var data []struct {
Region string `json:"region_name"`
Country string `json:"country_name"`
City string `json:"city_name"`
TCP string `json:"tcp"`
UDP string `json:"udp"`
}
if err := json.Unmarshal([]byte(s), &data); err != nil {
return nil, nil, err
}
sort.Slice(data, func(i, j int) bool {
if data[i].Region == data[j].Region {
if data[i].Country == data[j].Country {
return data[i].City < data[j].City
}
return data[i].Country < data[j].Country
}
return data[i].Region < data[j].Region
})
for _, jsonServer := range data {
if jsonServer.UDP == "" && jsonServer.TCP == "" {
warnings = append(warnings, fmt.Sprintf("server %s %s %s does not support TCP and UDP for openvpn", jsonServer.Region, jsonServer.Country, jsonServer.City))
continue
}
if jsonServer.UDP == "" || jsonServer.TCP == "" {
warnings = append(warnings, fmt.Sprintf("server %s %s %s does not support TCP or UDP for openvpn", jsonServer.Region, jsonServer.Country, jsonServer.City))
continue
}
host := jsonServer.UDP
const repetition = 3
IPs, err := resolveRepeat(ctx, lookupIP, host, repetition)
if err != nil {
warnings = append(warnings, err.Error())
continue
}
servers = append(servers, models.PurevpnServer{
Region: jsonServer.Region,
Country: jsonServer.Country,
City: jsonServer.City,
IPs: IPs,
})
}
return servers, warnings, nil
}
func stringifyPurevpnServers(servers []models.PurevpnServer) (s string) {
s = "func PurevpnServers() []models.PurevpnServer {\n"
s += " return []models.PurevpnServer{\n"
for _, server := range servers {
s += " " + server.String() + ",\n"
}
s += " }\n"
s += "}"
return s
}

View File

@@ -0,0 +1,41 @@
package updater
import (
"context"
"net"
)
func newResolver(resolverAddress string) *net.Resolver {
return &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{}
return d.DialContext(ctx, "udp", net.JoinHostPort(resolverAddress, "53"))
},
}
}
func newLookupIP(r *net.Resolver) lookupIPFunc {
return func(ctx context.Context, host string) (ips []net.IP, err error) {
addresses, err := r.LookupIPAddr(ctx, host)
if err != nil {
return nil, err
}
ips = make([]net.IP, len(addresses))
for i := range addresses {
ips[i] = addresses[i].IP
}
return ips, nil
}
}
func resolveRepeat(ctx context.Context, lookupIP lookupIPFunc, host string, n int) (ips []net.IP, err error) {
for i := 0; i < n; i++ {
newIPs, err := lookupIP(ctx, host)
if err != nil {
return nil, err
}
ips = append(ips, newIPs...)
}
return uniqueSortedIPs(ips), nil
}

View File

@@ -0,0 +1,249 @@
package updater
import (
"context"
"fmt"
"net"
"sort"
"strings"
"github.com/qdm12/gluetun/internal/models"
)
func (u *updater) updateSurfshark(ctx context.Context) (err error) {
servers, err := findSurfsharkServers(ctx, u.lookupIP)
if err != nil {
return fmt.Errorf("cannot update Surfshark servers: %w", err)
}
if u.options.Stdout {
u.println(stringifySurfsharkServers(servers))
}
u.servers.Surfshark.Timestamp = u.timeNow().Unix()
u.servers.Surfshark.Servers = servers
return nil
}
func findSurfsharkServers(ctx context.Context, lookupIP lookupIPFunc) (servers []models.SurfsharkServer, err error) {
const zipURL = "https://account.surfshark.com/api/v1/server/configurations"
contents, err := fetchAndExtractFiles(zipURL)
if err != nil {
return nil, err
}
for fileName, content := range contents {
if strings.HasSuffix(fileName, "_tcp.ovpn") {
continue // only parse UDP files
}
remoteLines := extractRemoteLinesFromOpenvpn(content)
if len(remoteLines) == 0 {
return nil, fmt.Errorf("cannot find any remote lines in %s", fileName)
}
hosts := extractHostnamesFromRemoteLines(remoteLines)
if len(hosts) == 0 {
return nil, fmt.Errorf("cannot find any hosts in %s", fileName)
}
var IPs []net.IP
var region string
for _, host := range hosts {
if net.ParseIP(host) != nil {
// only a few IP addresses, no idea for what region
// ignore them
continue
}
const repetition = 3
newIPs, err := resolveRepeat(ctx, lookupIP, host, repetition)
if err != nil {
return nil, err
}
IPs = append(IPs, newIPs...)
if region == "" {
subdomain := strings.TrimSuffix(host, ".prod.surfshark.com")
region = surfsharkSubdomainToRegion(subdomain)
}
}
if len(IPs) == 0 {
continue // only IPs, no hostnames found
}
if region == "" { // region not found in mapping
region = strings.TrimSuffix(hosts[0], ".prod.surfshark.com")
}
server := models.SurfsharkServer{
Region: region,
IPs: uniqueSortedIPs(IPs),
}
servers = append(servers, server)
}
sort.Slice(servers, func(i, j int) bool {
return servers[i].Region < servers[j].Region
})
return servers, nil
}
func stringifySurfsharkServers(servers []models.SurfsharkServer) (s string) {
s = "func SurfsharkServers() []models.SurfsharkServer {\n"
s += " return []models.SurfsharkServer{\n"
for _, server := range servers {
s += " " + server.String() + ",\n"
}
s += " }\n"
s += "}"
return s
}
func surfsharkSubdomainToRegion(subdomain string) (region string) {
return map[string]string{
"ae-dub": "United Arab Emirates",
"al-tia": "Albania",
"at-vie": "Austria",
"au-adl": "Australia Adelaide",
"au-bne": "Australia Brisbane",
"au-mel": "Australia Melbourne",
"au-per": "Australia Perth",
"au-syd": "Australia Sydney",
"au-us": "Australia US",
"az-bak": "Azerbaijan",
"ba-sjj": "Bosnia and Herzegovina",
"be-bru": "Belgium",
"bg-sof": "Bulgaria",
"br-sao": "Brazil",
"ca-mon": "Canada Montreal",
"ca-tor": "Canada Toronto",
"ca-us": "Canada US",
"ca-van": "Canada Vancouver",
"ch-zur": "Switzerland",
"cl-san": "Chile",
"co-bog": "Colombia",
"cr-sjn": "Costa Rica",
"cy-nic": "Cyprus",
"cz-prg": "Czech Republic",
"de-ber": "Germany Berlin",
"de-fra": "Germany Frankfurt am Main",
"de-fra-st001": "Germany Frankfurt am Main st001",
"de-fra-st002": "Germany Frankfurt am Main st002",
"de-fra-st003": "Germany Frankfurt am Main st003",
"de-muc": "Germany Munich",
"de-nue": "Germany Nuremberg",
"de-sg": "Germany Singapour",
"de-uk": "Germany UK",
"dk-cph": "Denmark",
"ee-tll": "Estonia",
"es-bcn": "Spain Barcelona",
"es-mad": "Spain Madrid",
"es-vlc": "Spain Valencia",
"fi-hel": "Finland",
"fr-bod": "France Bordeaux",
"fr-mrs": "France Marseilles",
"fr-par": "France Paris",
"fr-se": "France Sweden",
"gr-ath": "Greece",
"hk-hkg": "Hong Kong",
"hr-zag": "Croatia",
"hu-bud": "Hungary",
"id-jak": "Indonesia",
"ie-dub": "Ireland",
"il-tlv": "Israel",
"in-chn": "India Chennai",
"in-idr": "India Indore",
"in-mum": "India Mumbai",
"in-uk": "India UK",
"is-rkv": "Iceland",
"it-mil": "Italy Milan",
"it-rom": "Italy Rome",
"jp-tok": "Japan Tokyo",
"jp-tok-st001": "Japan Tokyo st001",
"jp-tok-st002": "Japan Tokyo st002",
"jp-tok-st003": "Japan Tokyo st003",
"jp-tok-st004": "Japan Tokyo st004",
"jp-tok-st005": "Japan Tokyo st005",
"jp-tok-st006": "Japan Tokyo st006",
"jp-tok-st007": "Japan Tokyo st007",
"kr-seo": "Korea",
"kz-ura": "Kazakhstan",
"lu-ste": "Luxembourg",
"lv-rig": "Latvia",
"ly-tip": "Libya",
"md-chi": "Moldova",
"mk-skp": "North Macedonia",
"my-kul": "Malaysia",
"ng-lag": "Nigeria",
"nl-ams": "Netherlands Amsterdam",
"nl-ams-st001": "Netherlands Amsterdam st001",
"nl-us": "Netherlands US",
"no-osl": "Norway",
"nz-akl": "New Zealand",
"ph-mnl": "Philippines",
"pl-gdn": "Poland Gdansk",
"pl-waw": "Poland Warsaw",
"pt-lis": "Portugal Lisbon",
"pt-lou": "Portugal Loule",
"pt-opo": "Portugal Porto",
"py-asu": "Paraguay",
"ro-buc": "Romania",
"rs-beg": "Serbia",
"ru-mos": "Russia Moscow",
"ru-spt": "Russia St. Petersburg",
"se-sto": "Sweden",
"sg-hk": "Singapore Hong Kong",
"sg-nl": "Singapore Netherlands",
"sg-sng": "Singapore",
"sg-in": "Singapore in",
"sg-sng-st001": "Singapore st001",
"sg-sng-st002": "Singapore st002",
"sg-sng-st003": "Singapore st003",
"sg-sng-st004": "Singapore st004",
"sg-sng-mp001": "Singapore mp001",
"si-lju": "Slovenia",
"sk-bts": "Slovekia",
"th-bkk": "Thailand",
"tr-bur": "Turkey",
"tw-tai": "Taiwan",
"ua-iev": "Ukraine",
"uk-de": "UK Germany",
"uk-fr": "UK France",
"uk-gla": "UK Glasgow",
"uk-lon": "UK London",
"uk-lon-mp001": "UK London mp001",
"uk-lon-st001": "UK London st001",
"uk-lon-st002": "UK London st002",
"uk-lon-st003": "UK London st003",
"uk-lon-st004": "UK London st004",
"uk-lon-st005": "UK London st005",
"uk-man": "UK Manchester",
"us-atl": "US Atlanta",
"us-bdn": "US Bend",
"us-bos": "US Boston",
"us-buf": "US Buffalo",
"us-chi": "US Chicago",
"us-clt": "US Charlotte",
"us-dal": "US Dallas",
"us-den": "US Denver",
"us-dtw": "US Gahanna",
"us-hou": "US Houston",
"us-kan": "US Kansas City",
"us-las": "US Las Vegas",
"us-lax": "US Los Angeles",
"us-ltm": "US Latham",
"us-mia": "US Miami",
"us-mnz": "US Maryland",
"us-nl": "US Netherlands",
"us-nyc": "US New York City",
"us-nyc-mp001": "US New York City mp001",
"us-nyc-st001": "US New York City st001",
"us-nyc-st002": "US New York City st002",
"us-nyc-st003": "US New York City st003",
"us-nyc-st004": "US New York City st004",
"us-nyc-st005": "US New York City st005",
"us-orl": "US Orlando",
"us-phx": "US Phoenix",
"us-pt": "US Portugal",
"us-sea": "US Seatle",
"us-sfo": "US San Francisco",
"us-slc": "US Salt Lake City",
"us-stl": "US Saint Louis",
"us-tpa": "US Tampa",
"vn-hcm": "Vietnam",
"za-jnb": "South Africa",
"ar-bua": "Argentina Buenos Aires",
"tr-ist": "Turkey Istanbul",
"mx-mex": "Mexico City Mexico",
}[subdomain]
}

View File

@@ -1,81 +1,112 @@
package updater
import (
"context"
"fmt"
"net/http"
"time"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/storage"
)
type Updater interface {
UpdateServers(options Options) error
UpdateServers(ctx context.Context) error
}
type updater struct {
// configuration
options Options
storage storage.Storage
timeNow func() time.Time
println func(s string)
httpGet func(url string) (resp *http.Response, err error)
// state
servers models.AllServers
// Functions for tests
timeNow func() time.Time
println func(s string)
httpGet httpGetFunc
lookupIP lookupIPFunc
}
func New(storage storage.Storage, httpClient *http.Client) Updater {
func New(options Options, storage storage.Storage, httpClient *http.Client) Updater {
if len(options.DNSAddress) == 0 {
options.DNSAddress = "1.1.1.1"
}
resolver := newResolver(options.DNSAddress)
return &updater{
storage: storage,
timeNow: time.Now,
println: func(s string) { fmt.Println(s) },
httpGet: httpClient.Get,
storage: storage,
timeNow: time.Now,
println: func(s string) { fmt.Println(s) },
httpGet: httpClient.Get,
lookupIP: newLookupIP(resolver),
options: options,
}
}
func (u *updater) UpdateServers(options Options) error {
// TODO parallelize DNS resolution
func (u *updater) UpdateServers(ctx context.Context) (err error) {
const writeSync = false
allServers, err := u.storage.SyncServers(constants.GetAllServers(), writeSync)
u.servers, err = u.storage.SyncServers(constants.GetAllServers(), writeSync)
if err != nil {
return fmt.Errorf("cannot update servers: %w", err)
}
if options.PIA {
const newServers = true
servers, err := findPIAServers(newServers)
if err != nil {
return fmt.Errorf("cannot update PIA servers: %w", err)
}
if options.Stdout {
u.println(stringifyPIAServers(servers))
}
allServers.Pia.Timestamp = u.timeNow().Unix()
allServers.Pia.Servers = servers
if u.options.Cyberghost {
u.updateCyberghost(ctx)
}
if options.PIAold {
const newServers = false
servers, err := findPIAServers(newServers)
if err != nil {
return fmt.Errorf("cannot update PIA old servers: %w", err)
if u.options.Mullvad {
if err := u.updateMullvad(); err != nil {
return err
}
if options.Stdout {
u.println(stringifyPIAOldServers(servers))
}
allServers.PiaOld.Timestamp = u.timeNow().Unix()
allServers.PiaOld.Servers = servers
}
if options.Mullvad {
servers, err := u.findMullvadServers()
if err != nil {
return fmt.Errorf("cannot update Mullvad servers: %w", err)
if u.options.Nordvpn {
// TODO support servers offering only TCP or only UDP
if err := u.updateNordvpn(); err != nil {
return err
}
if options.Stdout {
u.println(stringifyMullvadServers(servers))
}
allServers.Mullvad.Timestamp = u.timeNow().Unix()
allServers.Mullvad.Servers = servers
}
if options.File {
if err := u.storage.FlushToFile(allServers); err != nil {
if u.options.PIA {
if err := u.updatePIA(); err != nil {
return err
}
}
if u.options.PIAold {
if err := u.updatePIAOld(); err != nil {
return err
}
}
if u.options.Purevpn {
// TODO support servers offering only TCP or only UDP
if err := u.updatePurevpn(ctx); err != nil {
return err
}
}
if u.options.Surfshark {
if err := u.updateSurfshark(ctx); err != nil {
return err
}
}
if u.options.Vyprvpn {
if err := u.updateVyprvpn(ctx); err != nil {
return err
}
}
if u.options.Windscribe {
u.updateWindscribe(ctx)
}
if u.options.File {
if err := u.storage.FlushToFile(u.servers); err != nil {
return fmt.Errorf("cannot update servers: %w", err)
}
}

View File

@@ -0,0 +1,72 @@
package updater
import (
"context"
"fmt"
"net"
"sort"
"strings"
"github.com/qdm12/gluetun/internal/models"
)
func (u *updater) updateVyprvpn(ctx context.Context) (err error) {
servers, err := findVyprvpnServers(ctx, u.lookupIP)
if err != nil {
return fmt.Errorf("cannot update Vyprvpn servers: %w", err)
}
if u.options.Stdout {
u.println(stringifyVyprvpnServers(servers))
}
u.servers.Vyprvpn.Timestamp = u.timeNow().Unix()
u.servers.Vyprvpn.Servers = servers
return nil
}
func findVyprvpnServers(ctx context.Context, lookupIP lookupIPFunc) (servers []models.VyprvpnServer, err error) {
const zipURL = "https://support.vyprvpn.com/hc/article_attachments/360052617332/Vypr_OpenVPN_20200320.zip"
contents, err := fetchAndExtractFiles(zipURL)
if err != nil {
return nil, err
}
for fileName, content := range contents {
remoteLines := extractRemoteLinesFromOpenvpn(content)
if len(remoteLines) == 0 {
return nil, fmt.Errorf("cannot find any remote lines in %s", fileName)
}
hosts := extractHostnamesFromRemoteLines(remoteLines)
if len(hosts) == 0 {
return nil, fmt.Errorf("cannot find any hosts in %s", fileName)
}
var IPs []net.IP
for _, host := range hosts {
newIPs, err := lookupIP(ctx, host)
if err != nil {
return nil, err
}
IPs = append(IPs, newIPs...)
}
region := strings.TrimSuffix(fileName, ".ovpn")
region = strings.ReplaceAll(region, " - ", " ")
server := models.VyprvpnServer{
Region: region,
IPs: uniqueSortedIPs(IPs),
}
servers = append(servers, server)
}
sort.Slice(servers, func(i, j int) bool {
return servers[i].Region < servers[j].Region
})
return servers, nil
}
func stringifyVyprvpnServers(servers []models.VyprvpnServer) (s string) {
s = "func VyprvpnServers() []models.VyprvpnServer {\n"
s += " return []models.VyprvpnServer{\n"
for _, server := range servers {
s += " " + server.String() + ",\n"
}
s += " }\n"
s += "}"
return s
}

View File

@@ -0,0 +1,138 @@
package updater
import (
"context"
"sort"
"github.com/qdm12/gluetun/internal/models"
)
func (u *updater) updateWindscribe(ctx context.Context) {
servers := findWindscribeServers(ctx, u.lookupIP)
if u.options.Stdout {
u.println(stringifyWindscribeServers(servers))
}
u.servers.Windscribe.Timestamp = u.timeNow().Unix()
u.servers.Windscribe.Servers = servers
}
func findWindscribeServers(ctx context.Context, lookupIP lookupIPFunc) (servers []models.WindscribeServer) {
allCountryCodes := getCountryCodes()
windscribeCountryCodes := getWindscribeSubdomainToRegion()
possibleCountryCodes := mergeCountryCodes(windscribeCountryCodes, allCountryCodes)
const domain = "windscribe.com"
for countryCode, region := range possibleCountryCodes {
host := countryCode + "." + domain
ips, err := resolveRepeat(ctx, lookupIP, host, 2)
if err != nil || len(ips) == 0 {
continue
}
servers = append(servers, models.WindscribeServer{
Region: region,
IPs: ips,
})
}
sort.Slice(servers, func(i, j int) bool {
return servers[i].Region < servers[j].Region
})
return servers
}
func mergeCountryCodes(base, extend map[string]string) (merged map[string]string) {
merged = make(map[string]string, len(base))
for countryCode, region := range base {
merged[countryCode] = region
}
for countryCode := range base {
delete(extend, countryCode)
}
for countryCode, region := range extend {
merged[countryCode] = region
}
return merged
}
func stringifyWindscribeServers(servers []models.WindscribeServer) (s string) {
s = "func WindscribeServers() []models.WindscribeServer {\n"
s += " return []models.WindscribeServer{\n"
for _, server := range servers {
s += " " + server.String() + ",\n"
}
s += " }\n"
s += "}"
return s
}
func getWindscribeSubdomainToRegion() map[string]string {
return map[string]string{
"al": "Albania",
"ar": "Argentina",
"au": "Australia",
"at": "Austria",
"az": "Azerbaijan",
"be": "Belgium",
"ba": "Bosnia",
"br": "Brazil",
"bg": "Bulgaria",
"ca": "Canada East",
"ca-west": "Canada West",
"co": "Colombia",
"hr": "Croatia",
"cy": "Cyprus",
"cz": "Czech republic",
"dk": "Denmark",
"ee": "Estonia",
"aq": "Fake antarctica",
"fi": "Finland",
"fr": "France",
"ge": "Georgia",
"de": "Germany",
"gr": "Greece",
"hk": "Hong kong",
"hu": "Hungary",
"is": "Iceland",
"in": "India",
"id": "Indonesia",
"ie": "Ireland",
"il": "Israel",
"it": "Italy",
"jp": "Japan",
"lv": "Latvia",
"lt": "Lithuania",
"mk": "Macedonia",
"my": "Malaysia",
"mx": "Mexico",
"md": "Moldova",
"nl": "Netherlands",
"nz": "New zealand",
"no": "Norway",
"ph": "Philippines",
"pl": "Poland",
"pt": "Portugal",
"ro": "Romania",
"ru": "Russia",
"rs": "Serbia",
"sg": "Singapore",
"sk": "Slovakia",
"si": "Slovenia",
"za": "South Africa",
"kr": "South Korea",
"es": "Spain",
"se": "Sweden",
"ch": "Switzerland",
"th": "Thailand",
"tn": "Tunisia",
"tr": "Turkey",
"ua": "Ukraine",
"ae": "United Arab Emirates",
"uk": "United Kingdom",
"us-central": "US Central",
"us-east": "US East",
"us-west": "US West",
"vn": "Vietnam",
"wf-ca": "Windflix CA",
"wf-jp": "Windflix JP",
"wf-uk": "Windflix UK",
"wf-us": "Windflix US",
}
}