IPVanish support (#475)

- Fix #410 and #416
This commit is contained in:
Quentin McGaw
2021-06-20 09:21:48 -07:00
committed by GitHub
parent d81d4bbda3
commit 2c77b73ebc
38 changed files with 3245 additions and 6 deletions

3
.github/labels.yml vendored
View File

@@ -21,6 +21,9 @@
- name: ":cloud: HideMyAss" - name: ":cloud: HideMyAss"
color: "cfe8d4" color: "cfe8d4"
description: "" description: ""
- name: ":cloud: IPVanish"
color: "cfe8d4"
description: ""
- name: ":cloud: IVPN" - name: ":cloud: IVPN"
color: "cfe8d4" color: "cfe8d4"
description: "" description: ""

View File

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

View File

@@ -33,6 +33,7 @@ func (c *cli) Update(ctx context.Context, args []string, os os.OS, logger loggin
flagSet.BoolVar(&options.Cyberghost, "cyberghost", false, "Update Cyberghost servers") flagSet.BoolVar(&options.Cyberghost, "cyberghost", false, "Update Cyberghost servers")
flagSet.BoolVar(&options.Fastestvpn, "fastestvpn", false, "Update FastestVPN servers") flagSet.BoolVar(&options.Fastestvpn, "fastestvpn", false, "Update FastestVPN servers")
flagSet.BoolVar(&options.HideMyAss, "hidemyass", false, "Update HideMyAss servers") flagSet.BoolVar(&options.HideMyAss, "hidemyass", false, "Update HideMyAss servers")
flagSet.BoolVar(&options.Ipvanish, "ipvanish", false, "Update IpVanish servers")
flagSet.BoolVar(&options.Ivpn, "ivpn", false, "Update IVPN servers") flagSet.BoolVar(&options.Ivpn, "ivpn", false, "Update IVPN servers")
flagSet.BoolVar(&options.Mullvad, "mullvad", false, "Update Mullvad servers") flagSet.BoolVar(&options.Mullvad, "mullvad", false, "Update Mullvad servers")
flagSet.BoolVar(&options.Nordvpn, "nordvpn", false, "Update Nordvpn servers") flagSet.BoolVar(&options.Nordvpn, "nordvpn", false, "Update Nordvpn servers")

View File

@@ -0,0 +1,52 @@
package configuration
import (
"github.com/qdm12/gluetun/internal/constants"
)
func (settings *Provider) ipvanishLines() (lines []string) {
if len(settings.ServerSelection.Countries) > 0 {
lines = append(lines, lastIndent+"Countries: "+commaJoin(settings.ServerSelection.Countries))
}
if len(settings.ServerSelection.Cities) > 0 {
lines = append(lines, lastIndent+"Cities: "+commaJoin(settings.ServerSelection.Cities))
}
if len(settings.ServerSelection.Hostnames) > 0 {
lines = append(lines, lastIndent+"Hostnames: "+commaJoin(settings.ServerSelection.Hostnames))
}
return lines
}
func (settings *Provider) readIpvanish(r reader) (err error) {
settings.Name = constants.Ipvanish
settings.ServerSelection.TCP, err = readProtocol(r.env)
if err != nil {
return err
}
settings.ServerSelection.TargetIP, err = readTargetIP(r.env)
if err != nil {
return err
}
settings.ServerSelection.Countries, err = r.env.CSVInside("COUNTRY", constants.IpvanishCountryChoices())
if err != nil {
return err
}
settings.ServerSelection.Cities, err = r.env.CSVInside("CITY", constants.IpvanishCityChoices())
if err != nil {
return err
}
settings.ServerSelection.Hostnames, err = r.env.CSVInside("SERVER_HOSTNAME", constants.IpvanishHostnameChoices())
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,192 @@
package configuration
import (
"errors"
"net"
"testing"
"github.com/golang/mock/gomock"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/golibs/params/mock_params"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_Provider_ipvanishLines(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
settings Provider
lines []string
}{
"empty settings": {},
"full settings": {
settings: Provider{
ServerSelection: ServerSelection{
Countries: []string{"A", "B"},
Cities: []string{"C", "D"},
Hostnames: []string{"E", "F"},
},
},
lines: []string{
"|--Countries: A, B",
"|--Cities: C, D",
"|--Hostnames: E, F",
},
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
lines := testCase.settings.ipvanishLines()
assert.Equal(t, testCase.lines, lines)
})
}
}
func Test_Provider_readIpvanish(t *testing.T) {
t.Parallel()
var errDummy = errors.New("dummy test error")
type singleStringCall struct {
call bool
value string
err error
}
type sliceStringCall struct {
call bool
values []string
err error
}
testCases := map[string]struct {
protocol singleStringCall
targetIP singleStringCall
countries sliceStringCall
cities sliceStringCall
hostnames sliceStringCall
settings Provider
err error
}{
"protocol error": {
protocol: singleStringCall{call: true, err: errDummy},
settings: Provider{
Name: constants.Ipvanish,
},
err: errDummy,
},
"target IP error": {
protocol: singleStringCall{call: true},
targetIP: singleStringCall{call: true, value: "something", err: errDummy},
settings: Provider{
Name: constants.Ipvanish,
},
err: errDummy,
},
"countries error": {
protocol: singleStringCall{call: true},
targetIP: singleStringCall{call: true},
countries: sliceStringCall{call: true, err: errDummy},
settings: Provider{
Name: constants.Ipvanish,
},
err: errDummy,
},
"cities error": {
protocol: singleStringCall{call: true},
targetIP: singleStringCall{call: true},
countries: sliceStringCall{call: true},
cities: sliceStringCall{call: true, err: errDummy},
settings: Provider{
Name: constants.Ipvanish,
},
err: errDummy,
},
"hostnames error": {
protocol: singleStringCall{call: true},
targetIP: singleStringCall{call: true},
countries: sliceStringCall{call: true},
cities: sliceStringCall{call: true},
hostnames: sliceStringCall{call: true, err: errDummy},
settings: Provider{
Name: constants.Ipvanish,
},
err: errDummy,
},
"default settings": {
protocol: singleStringCall{call: true},
targetIP: singleStringCall{call: true},
countries: sliceStringCall{call: true},
cities: sliceStringCall{call: true},
hostnames: sliceStringCall{call: true},
settings: Provider{
Name: constants.Ipvanish,
},
},
"set settings": {
protocol: singleStringCall{call: true, value: constants.TCP},
targetIP: singleStringCall{call: true, value: "1.2.3.4"},
countries: sliceStringCall{call: true, values: []string{"A", "B"}},
cities: sliceStringCall{call: true, values: []string{"C", "D"}},
hostnames: sliceStringCall{call: true, values: []string{"E", "F"}},
settings: Provider{
Name: constants.Ipvanish,
ServerSelection: ServerSelection{
TCP: true,
TargetIP: net.IPv4(1, 2, 3, 4),
Countries: []string{"A", "B"},
Cities: []string{"C", "D"},
Hostnames: []string{"E", "F"},
},
},
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
env := mock_params.NewMockEnv(ctrl)
if testCase.protocol.call {
env.EXPECT().Inside("PROTOCOL", []string{constants.TCP, constants.UDP}, gomock.Any()).
Return(testCase.protocol.value, testCase.protocol.err)
}
if testCase.targetIP.call {
env.EXPECT().Get("OPENVPN_TARGET_IP").
Return(testCase.targetIP.value, testCase.targetIP.err)
}
if testCase.countries.call {
env.EXPECT().CSVInside("COUNTRY", constants.IpvanishCountryChoices()).
Return(testCase.countries.values, testCase.countries.err)
}
if testCase.cities.call {
env.EXPECT().CSVInside("CITY", constants.IpvanishCityChoices()).
Return(testCase.cities.values, testCase.cities.err)
}
if testCase.hostnames.call {
env.EXPECT().CSVInside("SERVER_HOSTNAME", constants.IpvanishHostnameChoices()).
Return(testCase.hostnames.values, testCase.hostnames.err)
}
r := reader{env: env}
var settings Provider
err := settings.readIpvanish(r)
if testCase.err != nil {
require.Error(t, err)
assert.Equal(t, testCase.err.Error(), err.Error())
} else {
assert.NoError(t, err)
}
assert.Equal(t, testCase.settings, settings)
})
}
}

View File

@@ -68,7 +68,7 @@ var (
func (settings *OpenVPN) read(r reader) (err error) { func (settings *OpenVPN) read(r reader) (err error) {
vpnsp, err := r.env.Inside("VPNSP", []string{ vpnsp, err := r.env.Inside("VPNSP", []string{
"cyberghost", "fastestvpn", "hidemyass", "ivpn", "mullvad", "nordvpn", "cyberghost", "fastestvpn", "hidemyass", "ipvanish", "ivpn", "mullvad", "nordvpn",
"privado", "pia", "private internet access", "privatevpn", "protonvpn", "privado", "pia", "private internet access", "privatevpn", "protonvpn",
"purevpn", "surfshark", "torguard", constants.VPNUnlimited, "vyprvpn", "windscribe"}, "purevpn", "surfshark", "torguard", constants.VPNUnlimited, "vyprvpn", "windscribe"},
params.Default("private internet access")) params.Default("private internet access"))
@@ -154,6 +154,8 @@ func (settings *OpenVPN) readProvider(r reader) error {
readProvider = settings.Provider.readFastestvpn readProvider = settings.Provider.readFastestvpn
case constants.HideMyAss: case constants.HideMyAss:
readProvider = settings.Provider.readHideMyAss readProvider = settings.Provider.readHideMyAss
case constants.Ipvanish:
readProvider = settings.Provider.readIpvanish
case constants.Ivpn: case constants.Ivpn:
readProvider = settings.Provider.readIvpn readProvider = settings.Provider.readIvpn
case constants.Mullvad: case constants.Mullvad:

View File

@@ -36,6 +36,8 @@ func (settings *Provider) lines() (lines []string) {
providerLines = settings.fastestvpnLines() providerLines = settings.fastestvpnLines()
case "hidemyass": case "hidemyass":
providerLines = settings.hideMyAssLines() providerLines = settings.hideMyAssLines()
case "ipvanish":
providerLines = settings.ipvanishLines()
case "ivpn": case "ivpn":
providerLines = settings.ivpnLines() providerLines = settings.ivpnLines()
case "mullvad": case "mullvad":

View File

@@ -73,6 +73,23 @@ func Test_Provider_lines(t *testing.T) {
" |--Hostnames: e, f", " |--Hostnames: e, f",
}, },
}, },
"ipvanish": {
settings: Provider{
Name: constants.Ipvanish,
ServerSelection: ServerSelection{
Countries: []string{"a", "b"},
Cities: []string{"c", "d"},
Hostnames: []string{"e", "f"},
},
},
lines: []string{
"|--Ipvanish settings:",
" |--Network protocol: udp",
" |--Countries: a, b",
" |--Cities: c, d",
" |--Hostnames: e, f",
},
},
"ivpn": { "ivpn": {
settings: Provider{ settings: Provider{
Name: constants.Ivpn, Name: constants.Ivpn,

View File

@@ -15,11 +15,11 @@ type ServerSelection struct { //nolint:maligned
// Cyberghost // Cyberghost
Group string `json:"group"` Group string `json:"group"`
// Fastestvpn, HideMyAss, IVPN, Mullvad, PrivateVPN, Protonvpn, PureVPN, VPNUnlimited // Fastestvpn, HideMyAss, IPVanish, IVPN, Mullvad, PrivateVPN, Protonvpn, PureVPN, VPNUnlimited
Countries []string `json:"countries"` Countries []string `json:"countries"`
// HideMyAss, IVPN, Mullvad, PrivateVPN, Protonvpn, PureVPN, VPNUnlimited, Windscribe // HideMyAss, IPVanish, IVPN, Mullvad, PrivateVPN, Protonvpn, PureVPN, VPNUnlimited, Windscribe
Cities []string `json:"cities"` Cities []string `json:"cities"`
// Fastestvpn, HideMyAss, IVPN, PrivateVPN, Windscribe, Privado, Protonvpn, VPNUnlimited // Fastestvpn, HideMyAss, IPVanish, IVPN, PrivateVPN, Windscribe, Privado, Protonvpn, VPNUnlimited
Hostnames []string `json:"hostnames"` Hostnames []string `json:"hostnames"`
Names []string `json:"names"` // Protonvpn Names []string `json:"names"` // Protonvpn

View File

@@ -13,6 +13,7 @@ type Updater struct {
Cyberghost bool `json:"cyberghost"` Cyberghost bool `json:"cyberghost"`
Fastestvpn bool `json:"fastestvpn"` Fastestvpn bool `json:"fastestvpn"`
HideMyAss bool `json:"hidemyass"` HideMyAss bool `json:"hidemyass"`
Ipvanish bool `json:"ipvanish"`
Ivpn bool `json:"ivpn"` Ivpn bool `json:"ivpn"`
Mullvad bool `json:"mullvad"` Mullvad bool `json:"mullvad"`
Nordvpn bool `json:"nordvpn"` Nordvpn bool `json:"nordvpn"`
@@ -50,6 +51,7 @@ func (settings *Updater) lines() (lines []string) {
func (settings *Updater) read(r reader) (err error) { func (settings *Updater) read(r reader) (err error) {
settings.Cyberghost = true settings.Cyberghost = true
settings.HideMyAss = true settings.HideMyAss = true
settings.Ipvanish = true
settings.Ivpn = true settings.Ivpn = true
settings.Mullvad = true settings.Mullvad = true
settings.Nordvpn = true settings.Nordvpn = true

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,11 @@ func GetAllServers() (allServers models.AllServers) {
Timestamp: 1620435633, Timestamp: 1620435633,
Servers: HideMyAssServers(), Servers: HideMyAssServers(),
}, },
Ipvanish: models.IpvanishServers{
Version: 1,
Timestamp: 1622430497,
Servers: IpvanishServers(),
},
Ivpn: models.IvpnServers{ Ivpn: models.IvpnServers{
Version: 1, Version: 1,
Timestamp: 1624120443, Timestamp: 1624120443,

View File

@@ -50,6 +50,11 @@ func Test_versions(t *testing.T) {
version: allServers.HideMyAss.Version, version: allServers.HideMyAss.Version,
digest: "a93b4057", digest: "a93b4057",
}, },
"Ipvanish": {
model: models.IpvanishServer{},
version: allServers.Ipvanish.Version,
digest: "2eb80d28",
},
"Ivpn": { "Ivpn": {
model: models.IvpnServer{}, model: models.IvpnServer{},
version: allServers.Ivpn.Version, version: allServers.Ivpn.Version,
@@ -167,6 +172,11 @@ func Test_timestamps(t *testing.T) {
timestamp: allServers.HideMyAss.Timestamp, timestamp: allServers.HideMyAss.Timestamp,
digest: "8f872ac4", digest: "8f872ac4",
}, },
"Ipvanish": {
servers: allServers.Ipvanish.Servers,
timestamp: allServers.Ipvanish.Timestamp,
digest: "c62dcf98",
},
"Ivpn": { "Ivpn": {
servers: allServers.Ivpn.Servers, servers: allServers.Ivpn.Servers,
timestamp: allServers.Ivpn.Timestamp, timestamp: allServers.Ivpn.Timestamp,

View File

@@ -7,6 +7,8 @@ const (
Fastestvpn = "fastestvpn" Fastestvpn = "fastestvpn"
// HideMyAss is a VPN provider. // HideMyAss is a VPN provider.
HideMyAss = "hidemyass" HideMyAss = "hidemyass"
// Ipvanish is a VPN provider.
Ipvanish = "ipvanish"
// Ivpn is a VPN provider. // Ivpn is a VPN provider.
Ivpn = "ivpn" Ivpn = "ivpn"
// Mullvad is a VPN provider. // Mullvad is a VPN provider.

View File

@@ -47,6 +47,20 @@ func (s *HideMyAssServer) String() string {
s.Country, s.Region, s.City, s.Hostname, s.TCP, s.UDP, goStringifyIPs(s.IPs)) s.Country, s.Region, s.City, s.Hostname, s.TCP, s.UDP, goStringifyIPs(s.IPs))
} }
type IpvanishServer struct {
Country string `json:"country"`
City string `json:"city"`
Hostname string `json:"hostname"`
TCP bool `json:"tcp"`
UDP bool `json:"udp"`
IPs []net.IP `json:"ips"`
}
func (s *IpvanishServer) String() string {
return fmt.Sprintf("{Country: %q, City: %q, Hostname: %q, TCP: %t, UDP: %t, IPs: %s}",
s.Country, s.City, s.Hostname, s.TCP, s.UDP, goStringifyIPs(s.IPs))
}
type IvpnServer struct { type IvpnServer struct {
Country string `json:"country"` Country string `json:"country"`
City string `json:"city"` City string `json:"city"`

View File

@@ -5,6 +5,7 @@ type AllServers struct {
Cyberghost CyberghostServers `json:"cyberghost"` Cyberghost CyberghostServers `json:"cyberghost"`
Fastestvpn FastestvpnServers `json:"fastestvpn"` Fastestvpn FastestvpnServers `json:"fastestvpn"`
HideMyAss HideMyAssServers `json:"hidemyass"` HideMyAss HideMyAssServers `json:"hidemyass"`
Ipvanish IpvanishServers `json:"ipvanish"`
Ivpn IvpnServers `json:"ivpn"` Ivpn IvpnServers `json:"ivpn"`
Mullvad MullvadServers `json:"mullvad"` Mullvad MullvadServers `json:"mullvad"`
Nordvpn NordvpnServers `json:"nordvpn"` Nordvpn NordvpnServers `json:"nordvpn"`
@@ -24,6 +25,7 @@ func (a *AllServers) Count() int {
return len(a.Cyberghost.Servers) + return len(a.Cyberghost.Servers) +
len(a.Fastestvpn.Servers) + len(a.Fastestvpn.Servers) +
len(a.HideMyAss.Servers) + len(a.HideMyAss.Servers) +
len(a.Ipvanish.Servers) +
len(a.Ivpn.Servers) + len(a.Ivpn.Servers) +
len(a.Mullvad.Servers) + len(a.Mullvad.Servers) +
len(a.Nordvpn.Servers) + len(a.Nordvpn.Servers) +
@@ -54,6 +56,11 @@ type HideMyAssServers struct {
Timestamp int64 `json:"timestamp"` Timestamp int64 `json:"timestamp"`
Servers []HideMyAssServer `json:"servers"` Servers []HideMyAssServer `json:"servers"`
} }
type IpvanishServers struct {
Version uint16 `json:"version"`
Timestamp int64 `json:"timestamp"`
Servers []IpvanishServer `json:"servers"`
}
type IvpnServers struct { type IvpnServers struct {
Version uint16 `json:"version"` Version uint16 `json:"version"`
Timestamp int64 `json:"timestamp"` Timestamp int64 `json:"timestamp"`

View File

@@ -0,0 +1,45 @@
package ipvanish
import (
"errors"
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/provider/utils"
)
var ErrProtocolUnsupported = errors.New("network protocol is not supported")
func (i *Ipvanish) GetOpenVPNConnection(selection configuration.ServerSelection) (
connection models.OpenVPNConnection, err error) {
const port = 443
const protocol = constants.UDP
if selection.TCP {
return connection, ErrProtocolUnsupported
}
servers, err := i.filterServers(selection)
if err != nil {
return connection, err
}
var connections []models.OpenVPNConnection
for _, server := range servers {
for _, IP := range server.IPs {
connection := models.OpenVPNConnection{
IP: IP,
Port: port,
Protocol: protocol,
Hostname: server.Hostname,
}
connections = append(connections, connection)
}
}
if selection.TargetIP != nil {
return utils.GetTargetIPConnection(connections, selection.TargetIP)
}
return utils.PickRandomConnection(connections, i.randSource), nil
}

View File

@@ -0,0 +1,29 @@
package ipvanish
import (
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/provider/utils"
)
func (i *Ipvanish) filterServers(selection configuration.ServerSelection) (
servers []models.IpvanishServer, err error) {
for _, server := range i.servers {
switch {
case
utils.FilterByPossibilities(server.Country, selection.Countries),
utils.FilterByPossibilities(server.City, selection.Cities),
utils.FilterByPossibilities(server.Hostname, selection.Hostnames),
selection.TCP && !server.TCP,
!selection.TCP && !server.UDP:
default:
servers = append(servers, server)
}
}
if len(servers) == 0 {
return nil, utils.NoServerFoundError(selection)
}
return servers, nil
}

View File

@@ -0,0 +1,65 @@
package ipvanish
import (
"strconv"
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/provider/utils"
)
func (i *Ipvanish) BuildConf(connection models.OpenVPNConnection,
username string, settings configuration.OpenVPN) (lines []string) {
if settings.Cipher == "" {
settings.Cipher = constants.AES256cbc
}
if settings.Auth == "" {
settings.Auth = constants.SHA256
}
lines = []string{
"client",
"dev tun",
"nobind",
"persist-key",
"ping-timer-rem",
"tls-exit",
// Ipvanish specific
"comp-lzo",
"tls-cipher TLS-DHE-RSA-WITH-AES-256-CBC-SHA:TLS-DHE-DSS-WITH-AES-256-CBC-SHA:TLS-RSA-WITH-AES-256-CBC-SHA",
"keysize 256",
// Added constant values
"mute-replay-warnings",
"auth-nocache",
"pull-filter ignore \"auth-token\"", // prevent auth failed loops
"auth-retry nointeract",
"suppress-timestamps",
// Modified variables
"verb " + strconv.Itoa(settings.Verbosity),
"auth-user-pass " + constants.OpenVPNAuthConf,
"proto " + connection.Protocol,
connection.RemoteLine(),
"verify-x509-name " + connection.Hostname, // + " name"
"auth " + settings.Auth,
}
lines = append(lines, utils.CipherLines(settings.Cipher, settings.Version)...)
if settings.MSSFix > 0 {
lines = append(lines, "mssfix "+strconv.Itoa(int(settings.MSSFix)))
}
if !settings.Root {
lines = append(lines, "user "+username)
}
lines = append(lines, utils.WrapOpenvpnCA(constants.IpvanishCA)...)
lines = append(lines, "")
return lines
}

View File

@@ -0,0 +1,17 @@
package ipvanish
import (
"context"
"net"
"net/http"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
func (i *Ipvanish) 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 Ipvanish")
}

View File

@@ -0,0 +1,19 @@
package ipvanish
import (
"math/rand"
"github.com/qdm12/gluetun/internal/models"
)
type Ipvanish struct {
servers []models.IpvanishServer
randSource rand.Source
}
func New(servers []models.IpvanishServer, randSource rand.Source) *Ipvanish {
return &Ipvanish{
servers: servers,
randSource: randSource,
}
}

View File

@@ -15,6 +15,7 @@ import (
"github.com/qdm12/gluetun/internal/provider/cyberghost" "github.com/qdm12/gluetun/internal/provider/cyberghost"
"github.com/qdm12/gluetun/internal/provider/fastestvpn" "github.com/qdm12/gluetun/internal/provider/fastestvpn"
"github.com/qdm12/gluetun/internal/provider/hidemyass" "github.com/qdm12/gluetun/internal/provider/hidemyass"
"github.com/qdm12/gluetun/internal/provider/ipvanish"
"github.com/qdm12/gluetun/internal/provider/ivpn" "github.com/qdm12/gluetun/internal/provider/ivpn"
"github.com/qdm12/gluetun/internal/provider/mullvad" "github.com/qdm12/gluetun/internal/provider/mullvad"
"github.com/qdm12/gluetun/internal/provider/nordvpn" "github.com/qdm12/gluetun/internal/provider/nordvpn"
@@ -50,6 +51,8 @@ func New(provider string, allServers models.AllServers, timeNow func() time.Time
return fastestvpn.New(allServers.Fastestvpn.Servers, randSource) return fastestvpn.New(allServers.Fastestvpn.Servers, randSource)
case constants.HideMyAss: case constants.HideMyAss:
return hidemyass.New(allServers.HideMyAss.Servers, randSource) return hidemyass.New(allServers.HideMyAss.Servers, randSource)
case constants.Ipvanish:
return ipvanish.New(allServers.Ipvanish.Servers, randSource)
case constants.Ivpn: case constants.Ivpn:
return ivpn.New(allServers.Ivpn.Servers, randSource) return ivpn.New(allServers.Ivpn.Servers, randSource)
case constants.Mullvad: case constants.Mullvad:

View File

@@ -36,6 +36,7 @@ func (s *storage) mergeServers(hardcoded, persisted models.AllServers) models.Al
Cyberghost: s.mergeCyberghost(hardcoded.Cyberghost, persisted.Cyberghost), Cyberghost: s.mergeCyberghost(hardcoded.Cyberghost, persisted.Cyberghost),
Fastestvpn: s.mergeFastestvpn(hardcoded.Fastestvpn, persisted.Fastestvpn), Fastestvpn: s.mergeFastestvpn(hardcoded.Fastestvpn, persisted.Fastestvpn),
HideMyAss: s.mergeHideMyAss(hardcoded.HideMyAss, persisted.HideMyAss), HideMyAss: s.mergeHideMyAss(hardcoded.HideMyAss, persisted.HideMyAss),
Ipvanish: s.mergeIpvanish(hardcoded.Ipvanish, persisted.Ipvanish),
Ivpn: s.mergeIvpn(hardcoded.Ivpn, persisted.Ivpn), Ivpn: s.mergeIvpn(hardcoded.Ivpn, persisted.Ivpn),
Mullvad: s.mergeMullvad(hardcoded.Mullvad, persisted.Mullvad), Mullvad: s.mergeMullvad(hardcoded.Mullvad, persisted.Mullvad),
Nordvpn: s.mergeNordVPN(hardcoded.Nordvpn, persisted.Nordvpn), Nordvpn: s.mergeNordVPN(hardcoded.Nordvpn, persisted.Nordvpn),
@@ -93,6 +94,19 @@ func (s *storage) mergeHideMyAss(hardcoded, persisted models.HideMyAssServers) m
return persisted return persisted
} }
func (s *storage) mergeIpvanish(hardcoded, persisted models.IpvanishServers) models.IpvanishServers {
if persisted.Timestamp <= hardcoded.Timestamp {
return hardcoded
}
versionDiff := hardcoded.Version - persisted.Version
if versionDiff > 0 {
s.logVersionDiff("Ipvanish", versionDiff)
return hardcoded
}
s.logTimeDiff("Ipvanish", persisted.Timestamp, hardcoded.Timestamp)
return persisted
}
func (s *storage) mergeIvpn(hardcoded, persisted models.IvpnServers) models.IvpnServers { func (s *storage) mergeIvpn(hardcoded, persisted models.IvpnServers) models.IvpnServers {
if persisted.Timestamp <= hardcoded.Timestamp { if persisted.Timestamp <= hardcoded.Timestamp {
return hardcoded return hardcoded

View File

@@ -21,6 +21,7 @@ func countServers(allServers models.AllServers) int {
return len(allServers.Cyberghost.Servers) + return len(allServers.Cyberghost.Servers) +
len(allServers.Fastestvpn.Servers) + len(allServers.Fastestvpn.Servers) +
len(allServers.HideMyAss.Servers) + len(allServers.HideMyAss.Servers) +
len(allServers.Ipvanish.Servers) +
len(allServers.Ivpn.Servers) + len(allServers.Ivpn.Servers) +
len(allServers.Mullvad.Servers) + len(allServers.Mullvad.Servers) +
len(allServers.Nordvpn.Servers) + len(allServers.Nordvpn.Servers) +

View File

@@ -8,6 +8,7 @@ import (
"github.com/qdm12/gluetun/internal/updater/providers/cyberghost" "github.com/qdm12/gluetun/internal/updater/providers/cyberghost"
"github.com/qdm12/gluetun/internal/updater/providers/fastestvpn" "github.com/qdm12/gluetun/internal/updater/providers/fastestvpn"
"github.com/qdm12/gluetun/internal/updater/providers/hidemyass" "github.com/qdm12/gluetun/internal/updater/providers/hidemyass"
"github.com/qdm12/gluetun/internal/updater/providers/ipvanish"
"github.com/qdm12/gluetun/internal/updater/providers/ivpn" "github.com/qdm12/gluetun/internal/updater/providers/ivpn"
"github.com/qdm12/gluetun/internal/updater/providers/mullvad" "github.com/qdm12/gluetun/internal/updater/providers/mullvad"
"github.com/qdm12/gluetun/internal/updater/providers/nordvpn" "github.com/qdm12/gluetun/internal/updater/providers/nordvpn"
@@ -77,6 +78,26 @@ func (u *updater) updateHideMyAss(ctx context.Context) (err error) {
return nil return nil
} }
func (u *updater) updateIpvanish(ctx context.Context) (err error) {
minServers := getMinServers(len(u.servers.Ipvanish.Servers))
servers, warnings, err := ipvanish.GetServers(
ctx, u.unzipper, u.presolver, minServers)
if u.options.CLI {
for _, warning := range warnings {
u.logger.Warn("Ipvanish: %s", warning)
}
}
if err != nil {
return err
}
if u.options.Stdout {
u.println(ipvanish.Stringify(servers))
}
u.servers.Ipvanish.Timestamp = u.timeNow().Unix()
u.servers.Ipvanish.Servers = servers
return nil
}
func (u *updater) updateIvpn(ctx context.Context) (err error) { func (u *updater) updateIvpn(ctx context.Context) (err error) {
minServers := getMinServers(len(u.servers.Ivpn.Servers)) minServers := getMinServers(len(u.servers.Ivpn.Servers))
servers, warnings, err := ivpn.GetServers( servers, warnings, err := ivpn.GetServers(

View File

@@ -0,0 +1,39 @@
package ipvanish
import (
"errors"
"fmt"
"strings"
"github.com/qdm12/gluetun/internal/constants"
)
var errCountryCodeUnknown = errors.New("country code is unknown")
func parseFilename(fileName, hostname string) (
country, city string, err error) {
const prefix = "ipvanish-"
s := strings.TrimPrefix(fileName, prefix)
const ext = ".ovpn"
host := strings.Split(hostname, ".")[0]
suffix := "-" + host + ext
s = strings.TrimSuffix(s, suffix)
parts := strings.Split(s, "-")
countryCodes := constants.CountryCodes()
countryCode := strings.ToLower(parts[0])
country, ok := countryCodes[countryCode]
if !ok {
return "", "", fmt.Errorf("%w: %s", errCountryCodeUnknown, countryCode)
}
country = strings.Title(country)
if len(parts) > 1 {
city = strings.Join(parts[1:], " ")
city = strings.Title(city)
}
return country, city, nil
}

View File

@@ -0,0 +1,54 @@
package ipvanish
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_parseFilename(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
fileName string
hostname string
country string
city string
err error
}{
"unknown country code": {
fileName: "ipvanish-unknown-host.ovpn",
hostname: "host.ipvanish.com",
err: errors.New("country code is unknown: unknown"),
},
"country code only": {
fileName: "ipvanish-ca-host.ovpn",
hostname: "host.ipvanish.com",
country: "Canada",
},
"country code and city": {
fileName: "ipvanish-ca-sao-paulo-host.ovpn",
hostname: "host.ipvanish.com",
country: "Canada",
city: "Sao Paulo",
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
country, city, err := parseFilename(testCase.fileName, testCase.hostname)
if testCase.err != nil {
require.Error(t, err)
assert.Equal(t, testCase.err.Error(), err.Error())
} else {
assert.NoError(t, err)
}
assert.Equal(t, testCase.country, country)
assert.Equal(t, testCase.city, city)
})
}
}

View File

@@ -0,0 +1,58 @@
package ipvanish
import (
"net"
"sort"
"github.com/qdm12/gluetun/internal/models"
)
type hostToServer map[string]models.IpvanishServer
func (hts hostToServer) add(host, country, city string, tcp, udp bool) {
server, ok := hts[host]
if !ok {
server.Hostname = host
server.Country = country
server.City = city
}
if tcp {
server.TCP = tcp
}
if udp {
server.UDP = udp
}
hts[host] = server
}
func (hts hostToServer) toHostsSlice() (hosts []string) {
hosts = make([]string, 0, len(hts))
for host := range hts {
hosts = append(hosts, host)
}
sort.Slice(hosts, func(i, j int) bool {
return hosts[i] < hosts[j]
})
return hosts
}
func (hts hostToServer) adaptWithIPs(hostToIPs map[string][]net.IP) {
for host, IPs := range hostToIPs {
server := hts[host]
server.IPs = IPs
hts[host] = server
}
for host, server := range hts {
if len(server.IPs) == 0 {
delete(hts, host)
}
}
}
func (hts hostToServer) toServersSlice() (servers []models.IpvanishServer) {
servers = make([]models.IpvanishServer, 0, len(hts))
for _, server := range hts {
servers = append(servers, server)
}
return servers
}

View File

@@ -0,0 +1,211 @@
package ipvanish
import (
"net"
"testing"
"github.com/qdm12/gluetun/internal/models"
"github.com/stretchr/testify/assert"
)
func Test_hostToServer_add(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
initialHTS hostToServer
host string
country string
city string
tcp bool
udp bool
expectedHTS hostToServer
}{
"empty host to server": {
initialHTS: hostToServer{},
host: "host",
country: "country",
city: "city",
tcp: true,
udp: true,
expectedHTS: hostToServer{
"host": {
Hostname: "host",
Country: "country",
City: "city",
TCP: true,
UDP: true,
},
},
},
"add server": {
initialHTS: hostToServer{
"existing host": {},
},
host: "host",
country: "country",
city: "city",
tcp: true,
udp: true,
expectedHTS: hostToServer{
"existing host": {},
"host": models.IpvanishServer{
Hostname: "host",
Country: "country",
City: "city",
TCP: true,
UDP: true,
},
},
},
"extend existing server": {
initialHTS: hostToServer{
"host": models.IpvanishServer{
Hostname: "host",
Country: "country",
City: "city",
TCP: true,
},
},
host: "host",
country: "country",
city: "city",
tcp: false,
udp: true,
expectedHTS: hostToServer{
"host": models.IpvanishServer{
Hostname: "host",
Country: "country",
City: "city",
TCP: true,
UDP: true,
},
},
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
testCase.initialHTS.add(testCase.host, testCase.country, testCase.city, testCase.tcp, testCase.udp)
assert.Equal(t, testCase.expectedHTS, testCase.initialHTS)
})
}
}
func Test_hostToServer_toHostsSlice(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
hts hostToServer
hosts []string
}{
"empty host to server": {
hts: hostToServer{},
hosts: []string{},
},
"single host": {
hts: hostToServer{
"A": {},
},
hosts: []string{"A"},
},
"multiple hosts": {
hts: hostToServer{
"A": {},
"B": {},
},
hosts: []string{"A", "B"},
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
hosts := testCase.hts.toHostsSlice()
assert.ElementsMatch(t, testCase.hosts, hosts)
})
}
}
func Test_hostToServer_adaptWithIPs(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
initialHTS hostToServer
hostToIPs map[string][]net.IP
expectedHTS hostToServer
}{
"create server": {
initialHTS: hostToServer{},
hostToIPs: map[string][]net.IP{
"A": {{1, 2, 3, 4}},
},
expectedHTS: hostToServer{
"A": models.IpvanishServer{
IPs: []net.IP{{1, 2, 3, 4}},
},
},
},
"add IPs to existing server": {
initialHTS: hostToServer{
"A": models.IpvanishServer{
Country: "country",
},
},
hostToIPs: map[string][]net.IP{
"A": {{1, 2, 3, 4}},
},
expectedHTS: hostToServer{
"A": models.IpvanishServer{
Country: "country",
IPs: []net.IP{{1, 2, 3, 4}},
},
},
},
"remove server without IP": {
initialHTS: hostToServer{
"A": models.IpvanishServer{
Country: "country",
},
},
hostToIPs: map[string][]net.IP{},
expectedHTS: hostToServer{},
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
testCase.initialHTS.adaptWithIPs(testCase.hostToIPs)
assert.Equal(t, testCase.expectedHTS, testCase.initialHTS)
})
}
}
func Test_hostToServer_toServersSlice(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
hts hostToServer
servers []models.IpvanishServer
}{
"empty host to server": {
hts: hostToServer{},
servers: []models.IpvanishServer{},
},
"multiple servers": {
hts: hostToServer{
"A": {Country: "A"},
"B": {Country: "B"},
},
servers: []models.IpvanishServer{
{Country: "A"},
{Country: "B"},
},
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
servers := testCase.hts.toServersSlice()
assert.ElementsMatch(t, testCase.servers, servers)
})
}
}

View File

@@ -0,0 +1,36 @@
package ipvanish
import (
"context"
"net"
"time"
"github.com/qdm12/gluetun/internal/updater/resolver"
)
func getResolveSettings(minServers int) (settings resolver.ParallelSettings) {
const (
maxFailRatio = 0.1
maxDuration = 20 * time.Second
betweenDuration = time.Second
maxNoNew = 2
maxFails = 2
)
return resolver.ParallelSettings{
MaxFailRatio: maxFailRatio,
MinFound: minServers,
Repeat: resolver.RepeatSettings{
MaxDuration: maxDuration,
BetweenDuration: betweenDuration,
MaxNoNew: maxNoNew,
MaxFails: maxFails,
},
}
}
func resolveHosts(ctx context.Context, presolver resolver.Parallel,
hosts []string, minServers int) (hostToIPs map[string][]net.IP,
warnings []string, err error) {
settings := getResolveSettings(minServers)
return presolver.Resolve(ctx, hosts, settings)
}

View File

@@ -0,0 +1,56 @@
package ipvanish
import (
"context"
"errors"
"net"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/qdm12/gluetun/internal/updater/resolver"
"github.com/qdm12/gluetun/internal/updater/resolver/mock_resolver"
"github.com/stretchr/testify/assert"
)
func Test_resolveHosts(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
ctx := context.Background()
presolver := mock_resolver.NewMockParallel(ctrl)
hosts := []string{"host1", "host2"}
const minServers = 10
expectedHostToIPs := map[string][]net.IP{
"host1": {{1, 2, 3, 4}},
"host2": {{2, 3, 4, 5}},
}
expectedWarnings := []string{"warning1", "warning2"}
expectedErr := errors.New("dummy")
const (
maxFailRatio = 0.1
maxDuration = 20 * time.Second
betweenDuration = time.Second
maxNoNew = 2
maxFails = 2
)
expectedSettings := resolver.ParallelSettings{
MaxFailRatio: maxFailRatio,
MinFound: minServers,
Repeat: resolver.RepeatSettings{
MaxDuration: maxDuration,
BetweenDuration: betweenDuration,
MaxNoNew: maxNoNew,
MaxFails: maxFails,
},
}
presolver.EXPECT().Resolve(ctx, hosts, expectedSettings).
Return(expectedHostToIPs, expectedWarnings, expectedErr)
hostToIPs, warnings, err := resolveHosts(ctx, presolver, hosts, minServers)
assert.Equal(t, expectedHostToIPs, hostToIPs)
assert.Equal(t, expectedWarnings, warnings)
assert.Equal(t, expectedErr, err)
}

View File

@@ -0,0 +1,92 @@
// Package ipvanish contains code to obtain the server information
// for the Surshark provider.
package ipvanish
import (
"context"
"errors"
"fmt"
"strings"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/updater/openvpn"
"github.com/qdm12/gluetun/internal/updater/resolver"
"github.com/qdm12/gluetun/internal/updater/unzip"
)
var ErrNotEnoughServers = errors.New("not enough servers found")
func GetServers(ctx context.Context, unzipper unzip.Unzipper,
presolver resolver.Parallel, minServers int) (
servers []models.IpvanishServer, warnings []string, err error) {
const url = "https://www.ipvanish.com/software/configs/configs.zip"
contents, err := unzipper.FetchAndExtract(ctx, url)
if err != nil {
return nil, nil, err
} else if len(contents) < minServers {
return nil, nil, fmt.Errorf("%w: %d and expected at least %d",
ErrNotEnoughServers, len(contents), minServers)
}
hts := make(hostToServer)
for fileName, content := range contents {
if !strings.HasSuffix(fileName, ".ovpn") {
continue // not an OpenVPN file
}
tcp, udp, err := openvpn.ExtractProto(content)
if err != nil {
// treat error as warning and go to next file
warning := err.Error() + ": in " + fileName
warnings = append(warnings, warning)
continue
}
hostname, warning, err := openvpn.ExtractHost(content)
if warning != "" {
warnings = append(warnings, warning)
}
if err != nil {
// treat error as warning and go to next file
warning := err.Error() + " in " + fileName
warnings = append(warnings, warning)
continue
}
country, city, err := parseFilename(fileName, hostname)
if err != nil {
// treat error as warning and go to next file
warning := err.Error() + " in " + fileName
warnings = append(warnings, warning)
continue
}
hts.add(hostname, country, city, tcp, udp)
}
if len(hts) < minServers {
return nil, warnings, fmt.Errorf("%w: %d and expected at least %d",
ErrNotEnoughServers, len(hts), minServers)
}
hosts := hts.toHostsSlice()
hostToIPs, newWarnings, err := resolveHosts(ctx, presolver, hosts, minServers)
warnings = append(warnings, newWarnings...)
if err != nil {
return nil, warnings, err
}
hts.adaptWithIPs(hostToIPs)
servers = hts.toServersSlice()
if len(servers) < minServers {
return nil, warnings, fmt.Errorf("%w: %d and expected at least %d",
ErrNotEnoughServers, len(servers), minServers)
}
sortServers(servers)
return servers, warnings, nil
}

View File

@@ -0,0 +1,150 @@
package ipvanish
import (
"context"
"errors"
"net"
"testing"
"github.com/golang/mock/gomock"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/updater/resolver"
"github.com/qdm12/gluetun/internal/updater/resolver/mock_resolver"
"github.com/qdm12/gluetun/internal/updater/unzip/mock_unzip"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_GetServers(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
// Inputs
minServers int
// Unzip
unzipContents map[string][]byte
unzipErr error
// Resolution
expectResolve bool
hostsToResolve []string
resolveSettings resolver.ParallelSettings
hostToIPs map[string][]net.IP
resolveWarnings []string
resolveErr error
// Output
servers []models.IpvanishServer
warnings []string
err error
}{
"unzipper error": {
unzipErr: errors.New("dummy"),
err: errors.New("dummy"),
},
"not enough unzip contents": {
minServers: 1,
unzipContents: map[string][]byte{},
err: errors.New("not enough servers found: 0 and expected at least 1"),
},
"no openvpn file": {
minServers: 1,
unzipContents: map[string][]byte{"somefile.txt": {}},
err: errors.New("not enough servers found: 0 and expected at least 1"),
},
"invalid proto": {
minServers: 1,
unzipContents: map[string][]byte{"badproto.ovpn": []byte(`proto invalid`)},
warnings: []string{"unknown protocol: invalid: in badproto.ovpn"},
err: errors.New("not enough servers found: 0 and expected at least 1"),
},
"no host": {
minServers: 1,
unzipContents: map[string][]byte{"nohost.ovpn": []byte(``)},
warnings: []string{"remote host not found in nohost.ovpn"},
err: errors.New("not enough servers found: 0 and expected at least 1"),
},
"multiple hosts": {
minServers: 1,
unzipContents: map[string][]byte{
"ipvanish-CA-City-A-hosta.ovpn": []byte("remote hosta\nremote hostb"),
},
expectResolve: true,
hostsToResolve: []string{"hosta"},
resolveSettings: getResolveSettings(1),
warnings: []string{"only using the first host \"hosta\" and discarding 1 other hosts"},
err: errors.New("not enough servers found: 0 and expected at least 1"),
},
"resolve error": {
unzipContents: map[string][]byte{
"ipvanish-CA-City-A-hosta.ovpn": []byte("remote hosta"),
},
expectResolve: true,
hostsToResolve: []string{"hosta"},
resolveSettings: getResolveSettings(0),
resolveWarnings: []string{"resolve warning"},
resolveErr: errors.New("dummy"),
warnings: []string{"resolve warning"},
err: errors.New("dummy"),
},
"filename parsing error": {
minServers: 1,
unzipContents: map[string][]byte{
"ipvanish-unknown-City-A-hosta.ovpn": []byte("remote hosta"),
},
warnings: []string{"country code is unknown: unknown in ipvanish-unknown-City-A-hosta.ovpn"},
err: errors.New("not enough servers found: 0 and expected at least 1"),
},
"success": {
minServers: 1,
unzipContents: map[string][]byte{
"ipvanish-CA-City-A-hosta.ovpn": []byte("remote hosta"),
"ipvanish-LU-City-B-hostb.ovpn": []byte("remote hostb"),
},
expectResolve: true,
hostsToResolve: []string{"hosta", "hostb"},
resolveSettings: getResolveSettings(1),
hostToIPs: map[string][]net.IP{
"hosta": {{1, 1, 1, 1}, {2, 2, 2, 2}},
"hostb": {{3, 3, 3, 3}, {4, 4, 4, 4}},
},
resolveWarnings: []string{"resolve warning"},
servers: []models.IpvanishServer{
{Country: "Canada", City: "City A", Hostname: "hosta", UDP: true, IPs: []net.IP{{1, 1, 1, 1}, {2, 2, 2, 2}}},
{Country: "Luxembourg", City: "City B", Hostname: "hostb", UDP: true, IPs: []net.IP{{3, 3, 3, 3}, {4, 4, 4, 4}}},
},
warnings: []string{"resolve warning"},
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
ctx := context.Background()
unzipper := mock_unzip.NewMockUnzipper(ctrl)
const zipURL = "https://www.ipvanish.com/software/configs/configs.zip"
unzipper.EXPECT().FetchAndExtract(ctx, zipURL).
Return(testCase.unzipContents, testCase.unzipErr)
presolver := mock_resolver.NewMockParallel(ctrl)
if testCase.expectResolve {
presolver.EXPECT().Resolve(ctx, testCase.hostsToResolve, testCase.resolveSettings).
Return(testCase.hostToIPs, testCase.resolveWarnings, testCase.resolveErr)
}
servers, warnings, err := GetServers(ctx, unzipper, presolver, testCase.minServers)
assert.Equal(t, testCase.servers, servers)
assert.Equal(t, testCase.warnings, warnings)
if testCase.err != nil {
require.Error(t, err)
assert.Equal(t, testCase.err.Error(), err.Error())
} else {
assert.NoError(t, err)
}
})
}
}

View File

@@ -0,0 +1,19 @@
package ipvanish
import (
"sort"
"github.com/qdm12/gluetun/internal/models"
)
func sortServers(servers []models.IpvanishServer) {
sort.Slice(servers, func(i, j int) bool {
if servers[i].Country == servers[j].Country {
if servers[i].City == servers[j].City {
return servers[i].Hostname < servers[j].Hostname
}
return servers[i].City < servers[j].City
}
return servers[i].Country < servers[j].Country
})
}

View File

@@ -0,0 +1,40 @@
package ipvanish
import (
"testing"
"github.com/qdm12/gluetun/internal/models"
"github.com/stretchr/testify/assert"
)
func Test_sortServers(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
initialServers []models.IpvanishServer
sortedServers []models.IpvanishServer
}{
"no server": {},
"sorted servers": {
initialServers: []models.IpvanishServer{
{Country: "B", City: "A", Hostname: "A"},
{Country: "A", City: "A", Hostname: "B"},
{Country: "A", City: "A", Hostname: "A"},
{Country: "A", City: "B", Hostname: "A"},
},
sortedServers: []models.IpvanishServer{
{Country: "A", City: "A", Hostname: "A"},
{Country: "A", City: "A", Hostname: "B"},
{Country: "A", City: "B", Hostname: "A"},
{Country: "B", City: "A", Hostname: "A"},
},
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
sortServers(testCase.initialServers)
assert.Equal(t, testCase.sortedServers, testCase.initialServers)
})
}
}

View File

@@ -0,0 +1,14 @@
package ipvanish
import "github.com/qdm12/gluetun/internal/models"
func Stringify(servers []models.IpvanishServer) (s string) {
s = "func IpvanishServers() []models.IpvanishServer {\n"
s += " return []models.IpvanishServer{\n"
for _, server := range servers {
s += " " + server.String() + ",\n"
}
s += " }\n"
s += "}"
return s
}

View File

@@ -0,0 +1,43 @@
package ipvanish
import (
"testing"
"github.com/qdm12/gluetun/internal/models"
"github.com/stretchr/testify/assert"
)
func Test_Stringify(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
servers []models.IpvanishServer
s string
}{
"no server": {
s: `func IpvanishServers() []models.IpvanishServer {
return []models.IpvanishServer{
}
}`,
},
"multiple servers": {
servers: []models.IpvanishServer{
{Country: "A"},
{Country: "B"},
},
s: `func IpvanishServers() []models.IpvanishServer {
return []models.IpvanishServer{
{Country: "A", City: "", Hostname: "", TCP: false, UDP: false, IPs: []net.IP{}},
{Country: "B", City: "", Hostname: "", TCP: false, UDP: false, IPs: []net.IP{}},
}
}`,
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
s := Stringify(testCase.servers)
assert.Equal(t, testCase.s, s)
})
}
}

View File

@@ -85,6 +85,16 @@ func (u *updater) UpdateServers(ctx context.Context) (allServers models.AllServe
} }
} }
if u.options.Ipvanish {
u.logger.Info("updating Ipvanish servers...")
if err := u.updateIpvanish(ctx); err != nil {
u.logger.Error(err)
}
if err := ctx.Err(); err != nil {
return allServers, err
}
}
if u.options.Ivpn { if u.options.Ivpn {
u.logger.Info("updating Ivpn servers...") u.logger.Info("updating Ivpn servers...")
if err := u.updateIvpn(ctx); err != nil { if err := u.updateIvpn(ctx); err != nil {