diff --git a/internal/provider/fastestvpn/provider.go b/internal/provider/fastestvpn/provider.go index 017cccea..497aa2e1 100644 --- a/internal/provider/fastestvpn/provider.go +++ b/internal/provider/fastestvpn/provider.go @@ -2,6 +2,7 @@ package fastestvpn import ( "math/rand" + "net/http" "github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/provider/common" @@ -15,12 +16,12 @@ type Provider struct { } func New(storage common.Storage, randSource rand.Source, - unzipper common.Unzipper, updaterWarner common.Warner, + client *http.Client, updaterWarner common.Warner, parallelResolver common.ParallelResolver) *Provider { return &Provider{ storage: storage, randSource: randSource, - Fetcher: updater.New(unzipper, updaterWarner, parallelResolver), + Fetcher: updater.New(client, updaterWarner, parallelResolver), } } diff --git a/internal/provider/fastestvpn/updater/api.go b/internal/provider/fastestvpn/updater/api.go new file mode 100644 index 00000000..39e35fc6 --- /dev/null +++ b/internal/provider/fastestvpn/updater/api.go @@ -0,0 +1,129 @@ +package updater + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/qdm12/gluetun/internal/provider/common" +) + +type apiServer struct { + country string + city string + hostname string +} + +var ( + ErrDataMalformed = errors.New("data is malformed") +) + +const apiURL = "https://support.fastestvpn.com/wp-admin/admin-ajax.php" + +// The API URL and requests are shamelessly taken from network operations +// done on the page https://support.fastestvpn.com/vpn-servers/ +func fetchAPIServers(ctx context.Context, client *http.Client, protocol string) ( + servers []apiServer, err error) { + form := url.Values{ + "action": []string{"vpn_servers"}, + "protocol": []string{protocol}, + } + body := strings.NewReader(form.Encode()) + + request, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, body) + if err != nil { + return nil, fmt.Errorf("creating request: %w", err) + } + request.Header.Set("Content-Type", "application/x-www-form-urlencoded") + // request.Header.Set("User-Agent", "curl/8.9.0") + // request.Header.Set("Accept", "*/*") + + response, err := client.Do(request) + if err != nil { + return nil, fmt.Errorf("sending request: %w", err) + } + + if response.StatusCode != http.StatusOK { + _ = response.Body.Close() + return nil, fmt.Errorf("%w: %d", common.ErrHTTPStatusCodeNotOK, response.StatusCode) + } + + data, err := io.ReadAll(response.Body) + if err != nil { + _ = response.Body.Close() + return nil, fmt.Errorf("reading response body: %w", err) + } + + err = response.Body.Close() + if err != nil { + return nil, fmt.Errorf("closing response body: %w", err) + } + + const usualMaxNumber = 100 + servers = make([]apiServer, 0, usualMaxNumber) + + for { + trBlock := getNextTRBlock(data) + if trBlock == nil { + break + } + data = data[len(trBlock):] + + var server apiServer + + const numberOfTDBlocks = 3 + for i := 0; i < numberOfTDBlocks; i++ { + tdBlock := getNextTDBlock(trBlock) + if tdBlock == nil { + return nil, fmt.Errorf("%w: expected 3 blocks in block %q", + ErrDataMalformed, string(trBlock)) + } + trBlock = trBlock[len(tdBlock):] + + const startToken, endToken = "", "" + tdBlockData := string(tdBlock[len(startToken) : len(tdBlock)-len(endToken)]) + const countryIndex, cityIndex, hostnameIndex = 0, 1, 2 + switch i { + case countryIndex: + server.country = tdBlockData + case cityIndex: + server.city = tdBlockData + case hostnameIndex: + server.hostname = tdBlockData + } + } + servers = append(servers, server) + } + + return servers, nil +} + +func getNextTRBlock(data []byte) (trBlock []byte) { + const startToken, endToken = "", "" + return getNextBlock(data, startToken, endToken) +} + +func getNextTDBlock(data []byte) (tdBlock []byte) { + const startToken, endToken = "", "" + return getNextBlock(data, startToken, endToken) +} + +func getNextBlock(data []byte, startToken, endToken string) (nextBlock []byte) { + i := bytes.Index(data, []byte(startToken)) + if i == -1 { + return nil + } + + nextBlock = data[i:] + i = bytes.Index(nextBlock[len(startToken):], []byte(endToken)) + if i == -1 { + return nil + } + nextBlock = nextBlock[:i+len(startToken)+len(endToken)] + return nextBlock +} diff --git a/internal/provider/fastestvpn/updater/api_test.go b/internal/provider/fastestvpn/updater/api_test.go new file mode 100644 index 00000000..ec49a887 --- /dev/null +++ b/internal/provider/fastestvpn/updater/api_test.go @@ -0,0 +1,164 @@ +package updater + +import ( + "context" + "errors" + "io" + "net/http" + "strings" + "testing" + + "github.com/qdm12/gluetun/internal/provider/common" + "github.com/stretchr/testify/assert" +) + +type roundTripFunc func(r *http.Request) (*http.Response, error) + +func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { + return f(r) +} + +func Test_fechAPIServers(t *testing.T) { + t.Parallel() + + errTest := errors.New("test error") + + testCases := map[string]struct { + ctx context.Context + protocol string + requestBody string + responseStatus int + responseBody io.ReadCloser + transportErr error + servers []apiServer + errWrapped error + errMessage string + }{ + "transport_error": { + ctx: context.Background(), + protocol: "tcp", + requestBody: "action=vpn_servers&protocol=tcp", + responseStatus: http.StatusOK, + transportErr: errTest, + errWrapped: errTest, + errMessage: `sending request: Post ` + + `"https://support.fastestvpn.com/wp-admin/admin-ajax.php": ` + + `test error`, + }, + "not_found_status_code": { + ctx: context.Background(), + protocol: "tcp", + requestBody: "action=vpn_servers&protocol=tcp", + responseStatus: http.StatusNotFound, + errWrapped: common.ErrHTTPStatusCodeNotOK, + errMessage: "HTTP status code not OK: 404", + }, + "empty_data": { + ctx: context.Background(), + protocol: "tcp", + requestBody: "action=vpn_servers&protocol=tcp", + responseStatus: http.StatusOK, + responseBody: io.NopCloser(strings.NewReader("")), + servers: []apiServer{}, + }, + "single_server": { + ctx: context.Background(), + protocol: "tcp", + requestBody: "action=vpn_servers&protocol=tcp", + responseStatus: http.StatusOK, + responseBody: io.NopCloser(strings.NewReader( + "irrelevantAustraliaSydney" + + "au-stream.jumptoserver.comirrelevant")), + servers: []apiServer{ + {country: "Australia", city: "Sydney", hostname: "au-stream.jumptoserver.com"}, + }, + }, + "two_servers": { + ctx: context.Background(), + protocol: "tcp", + requestBody: "action=vpn_servers&protocol=tcp", + responseStatus: http.StatusOK, + responseBody: io.NopCloser(strings.NewReader( + "AustraliaSydneyau-stream.jumptoserver.com" + + "AustraliaSydneyau-01.jumptoserver.com")), + servers: []apiServer{ + {country: "Australia", city: "Sydney", hostname: "au-stream.jumptoserver.com"}, + {country: "Australia", city: "Sydney", hostname: "au-01.jumptoserver.com"}, + }, + }, + } + + for name, testCase := range testCases { + testCase := testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + + client := &http.Client{ + Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) { + assert.Equal(t, apiURL, r.URL.String()) + requestBody, err := io.ReadAll(r.Body) + assert.NoError(t, err) + assert.Equal(t, testCase.requestBody, string(requestBody)) + if testCase.transportErr != nil { + return nil, testCase.transportErr + } + return &http.Response{ + StatusCode: testCase.responseStatus, + Body: testCase.responseBody, + }, nil + }), + } + + entries, err := fetchAPIServers(testCase.ctx, client, testCase.protocol) + + assert.ErrorIs(t, err, testCase.errWrapped) + if testCase.errWrapped != nil { + assert.EqualError(t, err, testCase.errMessage) + } + assert.Equal(t, testCase.servers, entries) + }) + } +} + +func Test_getNextBlock(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + data string + startToken string + endToken string + nextBlock []byte + }{ + "empty_data": { + startToken: "", + endToken: "", + }, + "start_token_not_found": { + data: "test", + startToken: "", + endToken: "", + }, + "end_token_not_found": { + data: "test", + startToken: "", + endToken: "", + }, + "block_found": { + data: "xytesttest2zx", + startToken: "", + endToken: "", + nextBlock: []byte("test"), + }, + } + + for name, testCase := range testCases { + testCase := testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + + nextBlock := getNextBlock([]byte(testCase.data), testCase.startToken, testCase.endToken) + + assert.Equal(t, testCase.nextBlock, nextBlock) + }) + } +} diff --git a/internal/provider/fastestvpn/updater/filename.go b/internal/provider/fastestvpn/updater/filename.go deleted file mode 100644 index 2940a7c8..00000000 --- a/internal/provider/fastestvpn/updater/filename.go +++ /dev/null @@ -1,39 +0,0 @@ -package updater - -import ( - "errors" - "fmt" - "regexp" - "strings" -) - -var errFilenameNoProtocolSuffix = errors.New("filename does not have a protocol suffix") - -var trailNumberExp = regexp.MustCompile(`[0-9]+$`) - -func parseFilename(fileName string) ( - country string, tcp, udp bool, err error, -) { - const ( - tcpSuffix = "-tcp.ovpn" - udpSuffix = "-udp.ovpn" - ) - var suffix string - switch { - case strings.HasSuffix(strings.ToLower(fileName), tcpSuffix): - suffix = tcpSuffix - tcp = true - case strings.HasSuffix(strings.ToLower(fileName), udpSuffix): - suffix = udpSuffix - udp = true - default: - return "", false, false, fmt.Errorf("%w: %s", - errFilenameNoProtocolSuffix, fileName) - } - - countryWithNumber := strings.TrimSuffix(fileName, suffix) - number := trailNumberExp.FindString(countryWithNumber) - country = countryWithNumber[:len(countryWithNumber)-len(number)] - - return country, tcp, udp, nil -} diff --git a/internal/provider/fastestvpn/updater/hosttoserver.go b/internal/provider/fastestvpn/updater/hosttoserver.go index 6deaf3bd..3f87b27c 100644 --- a/internal/provider/fastestvpn/updater/hosttoserver.go +++ b/internal/provider/fastestvpn/updater/hosttoserver.go @@ -9,12 +9,19 @@ import ( type hostToServer map[string]models.Server -func (hts hostToServer) add(host, country string, tcp, udp bool) { +func (hts hostToServer) add(host, country, city string, tcp, udp bool) { server, ok := hts[host] if !ok { server.VPN = vpn.OpenVPN server.Hostname = host server.Country = country + server.City = city + } + if city != "" { + // some servers are listed without the city although + // they are also listed with the city described, so update + // the city field. + server.City = city } if tcp { server.TCP = true diff --git a/internal/provider/fastestvpn/updater/servers.go b/internal/provider/fastestvpn/updater/servers.go index 41699c77..bdbff7b8 100644 --- a/internal/provider/fastestvpn/updater/servers.go +++ b/internal/provider/fastestvpn/updater/servers.go @@ -4,48 +4,26 @@ import ( "context" "fmt" "sort" - "strings" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/provider/common" - "github.com/qdm12/gluetun/internal/updater/openvpn" ) func (u *Updater) FetchServers(ctx context.Context, minServers int) ( servers []models.Server, err error) { - const url = "https://support.fastestvpn.com/download/fastestvpn_ovpn" - contents, err := u.unzipper.FetchAndExtract(ctx, url) - if err != nil { - return nil, err - } else if len(contents) < minServers { - return nil, fmt.Errorf("%w: %d and expected at least %d", - common.ErrNotEnoughServers, len(contents), minServers) - } - + protocols := []string{"tcp", "udp"} hts := make(hostToServer) - for fileName, content := range contents { - if !strings.HasSuffix(fileName, ".ovpn") { - continue // not an OpenVPN file - } - - country, tcp, udp, err := parseFilename(fileName) + for _, protocol := range protocols { + apiServers, err := fetchAPIServers(ctx, u.client, protocol) if err != nil { - u.warner.Warn(err.Error()) - continue + return nil, fmt.Errorf("fetching %s servers from API: %w", protocol, err) } - - host, warning, err := openvpn.ExtractHost(content) - if warning != "" { - u.warner.Warn(warning) + for _, apiServer := range apiServers { + tcp := protocol == "tcp" + udp := protocol == "udp" + hts.add(apiServer.hostname, apiServer.country, apiServer.city, tcp, udp) } - if err != nil { - // treat error as warning and go to next file - u.warner.Warn(err.Error() + " in " + fileName) - continue - } - - hts.add(host, country, tcp, udp) } if len(hts) < minServers { diff --git a/internal/provider/fastestvpn/updater/updater.go b/internal/provider/fastestvpn/updater/updater.go index 3fbee059..f15a2f3a 100644 --- a/internal/provider/fastestvpn/updater/updater.go +++ b/internal/provider/fastestvpn/updater/updater.go @@ -1,19 +1,21 @@ package updater import ( + "net/http" + "github.com/qdm12/gluetun/internal/provider/common" ) type Updater struct { - unzipper common.Unzipper + client *http.Client parallelResolver common.ParallelResolver warner common.Warner } -func New(unzipper common.Unzipper, warner common.Warner, +func New(client *http.Client, warner common.Warner, parallelResolver common.ParallelResolver) *Updater { return &Updater{ - unzipper: unzipper, + client: client, parallelResolver: parallelResolver, warner: warner, } diff --git a/internal/provider/providers.go b/internal/provider/providers.go index 37c9e735..a689e149 100644 --- a/internal/provider/providers.go +++ b/internal/provider/providers.go @@ -62,7 +62,7 @@ func NewProviders(storage Storage, timeNow func() time.Time, providers.Custom: custom.New(extractor), providers.Cyberghost: cyberghost.New(storage, randSource, parallelResolver), providers.Expressvpn: expressvpn.New(storage, randSource, unzipper, updaterWarner, parallelResolver), - providers.Fastestvpn: fastestvpn.New(storage, randSource, unzipper, updaterWarner, parallelResolver), + providers.Fastestvpn: fastestvpn.New(storage, randSource, client, updaterWarner, parallelResolver), providers.HideMyAss: hidemyass.New(storage, randSource, client, updaterWarner, parallelResolver), providers.Ipvanish: ipvanish.New(storage, randSource, unzipper, updaterWarner, parallelResolver), providers.Ivpn: ivpn.New(storage, randSource, client, updaterWarner, parallelResolver), diff --git a/internal/storage/servers.json b/internal/storage/servers.json index f4a2aae4..d531ecb1 100644 --- a/internal/storage/servers.json +++ b/internal/storage/servers.json @@ -23712,11 +23712,12 @@ }, "fastestvpn": { "version": 2, - "timestamp": 1669836047, + "timestamp": 1722350820, "servers": [ { "vpn": "openvpn", - "country": "australia", + "country": "Australia", + "city": "Sydney", "hostname": "au-01.jumptoserver.com", "tcp": true, "udp": true, @@ -23726,7 +23727,8 @@ }, { "vpn": "openvpn", - "country": "australia", + "country": "Australia", + "city": "Sydney", "hostname": "au-02.jumptoserver.com", "tcp": true, "udp": true, @@ -23736,7 +23738,30 @@ }, { "vpn": "openvpn", - "country": "austria", + "country": "Australia", + "city": "Sydney", + "hostname": "au-stream.jumptoserver.com", + "tcp": true, + "udp": true, + "ips": [ + "146.70.141.62" + ] + }, + { + "vpn": "openvpn", + "country": "Austria", + "city": "Vienna", + "hostname": "at-stream.jumptoserver.com", + "tcp": true, + "udp": true, + "ips": [ + "146.70.146.50" + ] + }, + { + "vpn": "openvpn", + "country": "Austria", + "city": "Wien", "hostname": "at-01.jumptoserver.com", "tcp": true, "udp": true, @@ -23746,7 +23771,8 @@ }, { "vpn": "openvpn", - "country": "belgium", + "country": "Belgium", + "city": "Brussel", "hostname": "bel-01.jumptoserver.com", "tcp": true, "udp": true, @@ -23756,37 +23782,32 @@ }, { "vpn": "openvpn", - "country": "belgium", - "hostname": "bel-02.jumptoserver.com", + "country": "Belgium", + "city": "Brussels", + "hostname": "bel-stream.jumptoserver.com", "tcp": true, "udp": true, "ips": [ - "193.9.114.211" + "193.9.114.210" ] }, { "vpn": "openvpn", - "country": "brazil", - "hostname": "br.jumptoserver.com", + "country": "Brazil", + "hostname": "br-cf.jumptoserver.com", "tcp": true, "udp": true, "ips": [ - "185.192.124.101" + "185.192.124.75", + "185.192.124.77", + "185.192.124.104", + "185.192.124.131" ] }, { "vpn": "openvpn", - "country": "brazil", - "hostname": "br2.jumptoserver.com", - "tcp": true, - "udp": true, - "ips": [ - "185.192.124.121" - ] - }, - { - "vpn": "openvpn", - "country": "bulgaria", + "country": "Bulgaria", + "city": "Sofia", "hostname": "bg-01.jumptoserver.com", "tcp": true, "udp": true, @@ -23796,7 +23817,8 @@ }, { "vpn": "openvpn", - "country": "canada", + "country": "Canada", + "city": "Montreal", "hostname": "ca-01.jumptoserver.com", "tcp": true, "udp": true, @@ -23806,7 +23828,8 @@ }, { "vpn": "openvpn", - "country": "canada-stream", + "country": "Canada", + "city": "Montreal", "hostname": "ca-stream.jumptoserver.com", "tcp": true, "udp": true, @@ -23816,7 +23839,18 @@ }, { "vpn": "openvpn", - "country": "colombia", + "country": "Canada", + "city": "Montreal", + "hostname": "hk-dbl.jumptoserver.com", + "udp": true, + "ips": [ + "193.239.86.4" + ] + }, + { + "vpn": "openvpn", + "country": "Colombia", + "city": "Bogota", "hostname": "clmb.jumptoserver.com", "tcp": true, "udp": true, @@ -23826,7 +23860,8 @@ }, { "vpn": "openvpn", - "country": "czechia", + "country": "Czech Republic", + "city": "Prague", "hostname": "cz-01.jumptoserver.com", "tcp": true, "udp": true, @@ -23836,7 +23871,7 @@ }, { "vpn": "openvpn", - "country": "denmark", + "country": "Denmark", "hostname": "dk-01.jumptoserver.com", "tcp": true, "udp": true, @@ -23846,17 +23881,19 @@ }, { "vpn": "openvpn", - "country": "finland", - "hostname": "fi.jumptoserver.com", + "country": "Finland", + "city": "Helsinki", + "hostname": "fi-p2p.jumptoserver.com", "tcp": true, "udp": true, "ips": [ - "37.143.129.152" + "37.143.129.237" ] }, { "vpn": "openvpn", - "country": "finland", + "country": "Finland", + "city": "Helsinki", "hostname": "fi2.jumptoserver.com", "tcp": true, "udp": true, @@ -23866,7 +23903,8 @@ }, { "vpn": "openvpn", - "country": "france", + "country": "France", + "city": "Paris", "hostname": "fr.jumptoserver.com", "tcp": true, "udp": true, @@ -23876,7 +23914,8 @@ }, { "vpn": "openvpn", - "country": "france-via-uk", + "country": "France", + "city": "Paris", "hostname": "uk-dbl.jumptoserver.com", "tcp": true, "udp": true, @@ -23886,7 +23925,30 @@ }, { "vpn": "openvpn", - "country": "germany", + "country": "Germany", + "city": "Dusseldorf", + "hostname": "de-dus1.jumptoserver.com", + "tcp": true, + "udp": true, + "ips": [ + "89.163.157.7" + ] + }, + { + "vpn": "openvpn", + "country": "Germany", + "city": "Dusseldorf", + "hostname": "de-dus2.jumptoserver.com", + "tcp": true, + "udp": true, + "ips": [ + "213.202.218.2" + ] + }, + { + "vpn": "openvpn", + "country": "Germany", + "city": "Frankfurt", "hostname": "de-01.jumptoserver.com", "tcp": true, "udp": true, @@ -23896,17 +23958,8 @@ }, { "vpn": "openvpn", - "country": "germany-dus", - "hostname": "de-dus2.jumptoserver.com", - "tcp": true, - "udp": true, - "ips": [ - "89.163.225.245" - ] - }, - { - "vpn": "openvpn", - "country": "greece", + "country": "Greece", + "city": "Athina", "hostname": "grc.jumptoserver.com", "tcp": true, "udp": true, @@ -23916,97 +23969,52 @@ }, { "vpn": "openvpn", - "country": "hongkong", + "country": "Hong Kong", + "city": "HK", "hostname": "hk.jumptoserver.com", "tcp": true, "udp": true, "ips": [ - "64.120.88.110" + "193.239.86.3" ] }, { "vpn": "openvpn", - "country": "hongkong", - "hostname": "hk2.jumptoserver.com", - "tcp": true, - "udp": true, - "ips": [ - "84.17.37.194" - ] - }, - { - "vpn": "openvpn", - "country": "hongkong", - "hostname": "hk3.jumptoserver.com", - "tcp": true, - "udp": true, - "ips": [ - "103.75.117.73" - ] - }, - { - "vpn": "openvpn", - "country": "hungary", + "country": "Hungary", + "city": "Budapest", "hostname": "hng.jumptoserver.com", "tcp": true, "udp": true, "ips": [ - "37.120.144.162" + "37.120.144.165" ] }, { "vpn": "openvpn", - "country": "india", - "hostname": "in1.jumptoserver.com", + "country": "India", + "city": "Mumbai", + "hostname": "in-vr.jumptoserver.com", "tcp": true, "udp": true, "ips": [ - "194.195.115.169" + "45.84.241.2" ] }, { "vpn": "openvpn", - "country": "india", - "hostname": "in2.jumptoserver.com", + "country": "Ireland", + "city": "Dublin", + "hostname": "ie-stream.jumptoserver.com", "tcp": true, "udp": true, "ips": [ - "45.84.241.3" + "146.70.130.218" ] }, { "vpn": "openvpn", - "country": "india-stream", - "hostname": "in4.jumptoserver.com", - "tcp": true, - "udp": true, - "ips": [ - "192.46.215.192" - ] - }, - { - "vpn": "openvpn", - "country": "india-stream", - "hostname": "in5.jumptoserver.com", - "tcp": true, - "udp": true, - "ips": [ - "194.195.118.230" - ] - }, - { - "vpn": "openvpn", - "country": "india-stream", - "hostname": "in6.jumptoserver.com", - "tcp": true, - "udp": true, - "ips": [ - "192.46.215.50" - ] - }, - { - "vpn": "openvpn", - "country": "ireland", + "country": "Ireland", + "city": "Dublin", "hostname": "ir.jumptoserver.com", "tcp": true, "udp": true, @@ -24016,17 +24024,8 @@ }, { "vpn": "openvpn", - "country": "italy", - "hostname": "it-01.jumptoserver.com", - "tcp": true, - "udp": true, - "ips": [ - "95.174.64.122" - ] - }, - { - "vpn": "openvpn", - "country": "italy-stream", + "country": "Italy", + "city": "Assago", "hostname": "it-stream.jumptoserver.com", "tcp": true, "udp": true, @@ -24036,7 +24035,19 @@ }, { "vpn": "openvpn", - "country": "japan", + "country": "Italy", + "city": "milan", + "hostname": "it-01.jumptoserver.com", + "tcp": true, + "udp": true, + "ips": [ + "95.174.64.122" + ] + }, + { + "vpn": "openvpn", + "country": "Japan", + "city": "Tokyo", "hostname": "jp-01.jumptoserver.com", "tcp": true, "udp": true, @@ -24046,18 +24057,9 @@ }, { "vpn": "openvpn", - "country": "japan", - "hostname": "jp-02.jumptoserver.com", - "tcp": true, - "udp": true, - "ips": [ - "146.70.138.108" - ] - }, - { - "vpn": "openvpn", - "country": "japan-stream", - "hostname": "jp-03.jumptoserver.com", + "country": "Japan", + "city": "Tokyo", + "hostname": "jp-stream.jumptoserver.com", "tcp": true, "udp": true, "ips": [ @@ -24066,67 +24068,60 @@ }, { "vpn": "openvpn", - "country": "luxembourg", + "country": "Luxembourg", "hostname": "lux1.jumptoserver.com", "tcp": true, "udp": true, "ips": [ - "92.223.105.18" + "5.253.204.43" ] }, { "vpn": "openvpn", - "country": "malaysia", - "hostname": "my.jumptoserver.com", + "country": "Malaysia", + "hostname": "my-cf.jumptoserver.com", "tcp": true, "udp": true, "ips": [ + "103.75.116.27", + "103.75.116.141", "103.75.116.180" ] }, { "vpn": "openvpn", - "country": "malaysia", - "hostname": "my2.jumptoserver.com", + "country": "Mexico", + "hostname": "mx-cf.jumptoserver.com", "tcp": true, "udp": true, "ips": [ - "103.75.116.141" + "147.78.1.112" ] }, { "vpn": "openvpn", - "country": "mexico", - "hostname": "mx.jumptoserver.com", + "country": "Netherlands", + "hostname": "nl-01.jumptoserver.com", "tcp": true, "udp": true, "ips": [ - "147.78.1.191" + "108.181.123.74" ] }, { "vpn": "openvpn", - "country": "netherlands", - "hostname": "nl.jumptoserver.com", + "country": "Netherlands", + "hostname": "nl-02.jumptoserver.com", "tcp": true, "udp": true, "ips": [ - "172.107.95.122" + "108.181.123.75" ] }, { "vpn": "openvpn", - "country": "netherlands", - "hostname": "nl2.jumptoserver.com", - "tcp": true, - "udp": true, - "ips": [ - "172.107.95.123" - ] - }, - { - "vpn": "openvpn", - "country": "norway", + "country": "Norway", + "city": "Oslo", "hostname": "nr-01.jumptoserver.com", "tcp": true, "udp": true, @@ -24136,7 +24131,8 @@ }, { "vpn": "openvpn", - "country": "poland", + "country": "Poland", + "city": "Gdansk", "hostname": "pl.jumptoserver.com", "tcp": true, "udp": true, @@ -24146,7 +24142,8 @@ }, { "vpn": "openvpn", - "country": "portugal", + "country": "Portugal", + "city": "Lisbon", "hostname": "pt.jumptoserver.com", "tcp": true, "udp": true, @@ -24156,7 +24153,8 @@ }, { "vpn": "openvpn", - "country": "romania", + "country": "Romania", + "city": "Bucharest", "hostname": "ro-01.jumptoserver.com", "tcp": true, "udp": true, @@ -24166,27 +24164,41 @@ }, { "vpn": "openvpn", - "country": "russia", - "hostname": "ru3.jumptoserver.com", + "country": "Romania", + "city": "Bucharest", + "hostname": "sg-dvpn.jumptoserver.com", "tcp": true, "udp": true, "ips": [ - "185.126.239.245" + "91.207.173.101" ] }, { "vpn": "openvpn", - "country": "russia", - "hostname": "ru4.jumptoserver.com", + "country": "Russia", + "city": "Moscow", + "hostname": "ru-vr.jumptoserver.com", "tcp": true, "udp": true, "ips": [ - "185.126.239.252" + "91.226.58.5" ] }, { "vpn": "openvpn", - "country": "serbia", + "country": "Serbia", + "city": "Belgrade", + "hostname": "jp-dvpn.jumptoserver.com", + "tcp": true, + "udp": true, + "ips": [ + "146.70.138.108" + ] + }, + { + "vpn": "openvpn", + "country": "Serbia", + "city": "Belgrade", "hostname": "rs-01.jumptoserver.com", "tcp": true, "udp": true, @@ -24196,7 +24208,8 @@ }, { "vpn": "openvpn", - "country": "singapore", + "country": "Singapore", + "city": "Singapore", "hostname": "sg-01.jumptoserver.com", "tcp": true, "udp": true, @@ -24206,7 +24219,8 @@ }, { "vpn": "openvpn", - "country": "slovakia", + "country": "Slovakia", + "city": "Bratislava", "hostname": "svk.jumptoserver.com", "tcp": true, "udp": true, @@ -24216,7 +24230,19 @@ }, { "vpn": "openvpn", - "country": "southkorea", + "country": "South Africa", + "city": "Johannesburg", + "hostname": "sa-jn.jumptoserver.com", + "tcp": true, + "udp": true, + "ips": [ + "129.232.176.170" + ] + }, + { + "vpn": "openvpn", + "country": "South Korea", + "city": "Seoul", "hostname": "kr.jumptoserver.com", "tcp": true, "udp": true, @@ -24226,17 +24252,19 @@ }, { "vpn": "openvpn", - "country": "spain", + "country": "Spain", + "city": "Barcelona", "hostname": "es-01.jumptoserver.com", "tcp": true, "udp": true, "ips": [ - "172.107.242.178" + "108.181.71.242" ] }, { "vpn": "openvpn", - "country": "sweden", + "country": "Sweden", + "city": "Stockholm", "hostname": "se-01.jumptoserver.com", "tcp": true, "udp": true, @@ -24246,7 +24274,8 @@ }, { "vpn": "openvpn", - "country": "switzerland", + "country": "Switzerland", + "city": "Zurich", "hostname": "ch-01.jumptoserver.com", "tcp": true, "udp": true, @@ -24256,17 +24285,19 @@ }, { "vpn": "openvpn", - "country": "turkey", + "country": "Turkey", + "city": "Izmir", "hostname": "tr.jumptoserver.com", "tcp": true, "udp": true, "ips": [ - "185.123.102.57" + "185.123.101.43" ] }, { "vpn": "openvpn", - "country": "uae-dubai", + "country": "UAE", + "city": "Dubai", "hostname": "uae.jumptoserver.com", "tcp": true, "udp": true, @@ -24276,7 +24307,8 @@ }, { "vpn": "openvpn", - "country": "uk", + "country": "UK", + "city": "London", "hostname": "uk-01.jumptoserver.com", "tcp": true, "udp": true, @@ -24286,7 +24318,8 @@ }, { "vpn": "openvpn", - "country": "uk", + "country": "UK", + "city": "London", "hostname": "uk-02.jumptoserver.com", "tcp": true, "udp": true, @@ -24296,77 +24329,7 @@ }, { "vpn": "openvpn", - "country": "uk-stream", - "hostname": "uk-streaming.jumptoserver.com", - "tcp": true, - "udp": true, - "ips": [ - "195.191.219.70" - ] - }, - { - "vpn": "openvpn", - "country": "usa-ashburn", - "hostname": "us-ash.jumptoserver.com", - "tcp": true, - "udp": true, - "ips": [ - "172.106.16.62" - ] - }, - { - "vpn": "openvpn", - "country": "usa-atlanta", - "hostname": "us-at.jumptoserver.com", - "tcp": true, - "udp": true, - "ips": [ - "167.160.88.250" - ] - }, - { - "vpn": "openvpn", - "country": "usa-dallas", - "hostname": "us-dl.jumptoserver.com", - "tcp": true, - "udp": true, - "ips": [ - "172.106.1.66" - ] - }, - { - "vpn": "openvpn", - "country": "usa-denver", - "hostname": "us-dv1.jumptoserver.com", - "tcp": true, - "udp": true, - "ips": [ - "139.28.179.82" - ] - }, - { - "vpn": "openvpn", - "country": "usa-losangeles", - "hostname": "us-la.jumptoserver.com", - "tcp": true, - "udp": true, - "ips": [ - "109.236.60.123" - ] - }, - { - "vpn": "openvpn", - "country": "usa-losangeles", - "hostname": "us-la2.jumptoserver.com", - "tcp": true, - "udp": true, - "ips": [ - "109.236.60.124" - ] - }, - { - "vpn": "openvpn", - "country": "usa-miami", + "country": "USA", "hostname": "us-mia.jumptoserver.com", "tcp": true, "udp": true, @@ -24376,72 +24339,143 @@ }, { "vpn": "openvpn", - "country": "usa-newyork", - "hostname": "us-ny.jumptoserver.com", + "country": "USA", + "hostname": "us-wt1.jumptoserver.com", "tcp": true, "udp": true, "ips": [ - "173.208.96.143" + "23.105.160.221" ] }, { "vpn": "openvpn", - "country": "usa-seattle", - "hostname": "us-se1.jumptoserver.com", - "tcp": true, - "udp": true, - "ips": [ - "216.244.81.174" - ] - }, - { - "vpn": "openvpn", - "country": "usa-stream", - "hostname": "us-stream.jumptoserver.com", - "tcp": true, - "udp": true, - "ips": [ - "172.106.16.58" - ] - }, - { - "vpn": "openvpn", - "country": "usa-stream", - "hostname": "us-stream2.jumptoserver.com", - "tcp": true, - "udp": true, - "ips": [ - "172.106.1.67" - ] - }, - { - "vpn": "openvpn", - "country": "usa-via-netherlands", + "country": "USA", + "city": "Anchorage", "hostname": "nl-dbl.jumptoserver.com", "tcp": true, "udp": true, "ips": [ - "172.107.95.125" + "108.181.123.76" ] }, { "vpn": "openvpn", - "country": "usa-washington", - "hostname": "us-wt.jumptoserver.com", + "country": "USA", + "city": "Ashburn", + "hostname": "us-ash.jumptoserver.com", "tcp": true, "udp": true, "ips": [ - "23.105.160.235" + "108.181.56.98" ] }, { "vpn": "openvpn", - "country": "usa-washington", - "hostname": "us-wt2.jumptoserver.com", + "country": "USA", + "city": "Atlanta", + "hostname": "us-at.jumptoserver.com", "tcp": true, "udp": true, "ips": [ - "23.82.15.91" + "167.160.88.250" + ] + }, + { + "vpn": "openvpn", + "country": "USA", + "city": "Chicago", + "hostname": "us-ch.jumptoserver.com", + "udp": true, + "ips": [ + "88.216.97.2" + ] + }, + { + "vpn": "openvpn", + "country": "USA", + "city": "Dallas", + "hostname": "us-dl.jumptoserver.com", + "tcp": true, + "udp": true, + "ips": [ + "108.181.92.50" + ] + }, + { + "vpn": "openvpn", + "country": "USA", + "city": "Denver", + "hostname": "us-dv1.jumptoserver.com", + "tcp": true, + "udp": true, + "ips": [ + "139.28.179.82" + ] + }, + { + "vpn": "openvpn", + "country": "USA", + "city": "Fargo", + "hostname": "us-ash-stream.jumptoserver.com", + "tcp": true, + "udp": true, + "ips": [ + "108.181.56.100" + ] + }, + { + "vpn": "openvpn", + "country": "USA", + "city": "New York", + "hostname": "us-ny.jumptoserver.com", + "tcp": true, + "udp": true, + "ips": [ + "173.208.96.145" + ] + }, + { + "vpn": "openvpn", + "country": "USA", + "city": "Seattle", + "hostname": "us-se.jumptoserver.com", + "tcp": true, + "udp": true, + "ips": [ + "64.42.176.74" + ] + }, + { + "vpn": "openvpn", + "country": "Ukraine", + "city": "Kyyiv", + "hostname": "ur-kv.jumptoserver.com", + "tcp": true, + "udp": true, + "ips": [ + "176.103.54.79" + ] + }, + { + "vpn": "openvpn", + "country": "United Kingdom", + "city": "London", + "hostname": "uk-streaming.jumptoserver.com", + "tcp": true, + "udp": true, + "ips": [ + "195.191.219.70" + ] + }, + { + "vpn": "openvpn", + "country": "United Kingdom", + "city": "London", + "hostname": "us-dbl1.jumptoserver.com", + "tcp": true, + "udp": true, + "ips": [ + "139.28.179.83" ] } ]