feat(fastestvpn): Wireguard support (#2383)

Credits to @Zerauskire for the initial investigation and @jvanderzande for an initial implementation as well as reviewing the pull request
This commit is contained in:
Quentin McGaw
2024-07-31 16:16:50 +02:00
committed by GitHub
parent 7bc2972b27
commit 13ffffb157
10 changed files with 800 additions and 63 deletions

View File

@@ -60,7 +60,7 @@ Lightweight swiss-knife-like VPN client to multiple VPN service providers
- Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers - Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers
- Supports OpenVPN for all providers listed - Supports OpenVPN for all providers listed
- Supports Wireguard both kernelspace and userspace - Supports Wireguard both kernelspace and userspace
- For **AirVPN**, **Ivpn**, **Mullvad**, **NordVPN**, **Perfect privacy**, **Surfshark** and **Windscribe** - For **AirVPN**, **FastestVPN**, **Ivpn**, **Mullvad**, **NordVPN**, **Perfect privacy**, **Surfshark** and **Windscribe**
- For **ProtonVPN**, **PureVPN**, **Torguard**, **VPN Unlimited** and **WeVPN** using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md) - For **ProtonVPN**, **PureVPN**, **Torguard**, **VPN Unlimited** and **WeVPN** using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
- For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md) - For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md)
- More in progress, see [#134](https://github.com/qdm12/gluetun/issues/134) - More in progress, see [#134](https://github.com/qdm12/gluetun/issues/134)

View File

@@ -35,6 +35,7 @@ func (p *Provider) validate(vpnType string, storage Storage) (err error) {
validNames = []string{ validNames = []string{
providers.Airvpn, providers.Airvpn,
providers.Custom, providers.Custom,
providers.Fastestvpn,
providers.Ivpn, providers.Ivpn,
providers.Mullvad, providers.Mullvad,
providers.Nordvpn, providers.Nordvpn,

View File

@@ -58,6 +58,7 @@ func (w Wireguard) validate(vpnProvider string, ipv6Supported bool) (err error)
if !helpers.IsOneOf(vpnProvider, if !helpers.IsOneOf(vpnProvider,
providers.Airvpn, providers.Airvpn,
providers.Custom, providers.Custom,
providers.Fastestvpn,
providers.Ivpn, providers.Ivpn,
providers.Mullvad, providers.Mullvad,
providers.Nordvpn, providers.Nordvpn,

View File

@@ -38,8 +38,9 @@ type WireguardSelection struct {
func (w WireguardSelection) validate(vpnProvider string) (err error) { func (w WireguardSelection) validate(vpnProvider string) (err error) {
// Validate EndpointIP // Validate EndpointIP
switch vpnProvider { switch vpnProvider {
case providers.Airvpn, providers.Ivpn, providers.Mullvad, case providers.Airvpn, providers.Fastestvpn, providers.Ivpn,
providers.Nordvpn, providers.Surfshark, providers.Windscribe: providers.Mullvad, providers.Nordvpn, providers.Surfshark,
providers.Windscribe:
// endpoint IP addresses are baked in // endpoint IP addresses are baked in
case providers.Custom: case providers.Custom:
if !w.EndpointIP.IsValid() || w.EndpointIP.IsUnspecified() { if !w.EndpointIP.IsValid() || w.EndpointIP.IsUnspecified() {
@@ -56,7 +57,7 @@ func (w WireguardSelection) validate(vpnProvider string) (err error) {
return fmt.Errorf("%w", ErrWireguardEndpointPortNotSet) return fmt.Errorf("%w", ErrWireguardEndpointPortNotSet)
} }
// EndpointPort cannot be set // EndpointPort cannot be set
case providers.Surfshark, providers.Nordvpn: case providers.Fastestvpn, providers.Surfshark, providers.Nordvpn:
if *w.EndpointPort != 0 { if *w.EndpointPort != 0 {
return fmt.Errorf("%w", ErrWireguardEndpointPortSet) return fmt.Errorf("%w", ErrWireguardEndpointPortSet)
} }
@@ -89,7 +90,7 @@ func (w WireguardSelection) validate(vpnProvider string) (err error) {
// Validate PublicKey // Validate PublicKey
switch vpnProvider { switch vpnProvider {
case providers.Ivpn, providers.Mullvad, case providers.Fastestvpn, providers.Ivpn, providers.Mullvad,
providers.Surfshark, providers.Windscribe: providers.Surfshark, providers.Windscribe:
// public keys are baked in // public keys are baked in
case providers.Custom: case providers.Custom:

View File

@@ -113,7 +113,7 @@ func getMarkdownHeaders(vpnProvider string) (headers []string) {
case providers.Expressvpn: case providers.Expressvpn:
return []string{countryHeader, cityHeader, hostnameHeader, tcpHeader, udpHeader} return []string{countryHeader, cityHeader, hostnameHeader, tcpHeader, udpHeader}
case providers.Fastestvpn: case providers.Fastestvpn:
return []string{countryHeader, hostnameHeader, tcpHeader, udpHeader} return []string{countryHeader, hostnameHeader, vpnHeader, tcpHeader, udpHeader}
case providers.HideMyAss: case providers.HideMyAss:
return []string{countryHeader, regionHeader, cityHeader, hostnameHeader, tcpHeader, udpHeader} return []string{countryHeader, regionHeader, cityHeader, hostnameHeader, tcpHeader, udpHeader}
case providers.Ipvanish: case providers.Ipvanish:

View File

@@ -4,6 +4,7 @@ import (
"testing" "testing"
"github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -32,14 +33,14 @@ func Test_Servers_ToMarkdown(t *testing.T) {
provider: providers.Fastestvpn, provider: providers.Fastestvpn,
servers: Servers{ servers: Servers{
Servers: []Server{ Servers: []Server{
{Country: "a", Hostname: "xa", TCP: true}, {Country: "a", Hostname: "xa", VPN: vpn.OpenVPN, TCP: true},
{Country: "b", Hostname: "xb", UDP: true}, {Country: "b", Hostname: "xb", VPN: vpn.OpenVPN, UDP: true},
}, },
}, },
expectedMarkdown: "| Country | Hostname | TCP | UDP |\n" + expectedMarkdown: "| Country | Hostname | VPN | TCP | UDP |\n" +
"| --- | --- | --- | --- |\n" + "| --- | --- | --- | --- | --- |\n" +
"| a | `xa` | ✅ | ❌ |\n" + "| a | `xa` | openvpn | ✅ | ❌ |\n" +
"| b | `xb` | ❌ | ✅ |\n", "| b | `xb` | openvpn | ❌ | ✅ |\n",
}, },
} }

View File

@@ -8,7 +8,7 @@ import (
func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) ( func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) (
connection models.Connection, err error) { connection models.Connection, err error) {
defaults := utils.NewConnectionDefaults(4443, 4443, 0) //nolint:gomnd defaults := utils.NewConnectionDefaults(4443, 4443, 51820) //nolint:gomnd
return utils.GetConnection(p.Name(), return utils.GetConnection(p.Name(),
p.storage, selection, defaults, ipv6Supported, p.randSource) p.storage, selection, defaults, ipv6Supported, p.randSource)
} }

View File

@@ -7,32 +7,45 @@ import (
"github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/models"
) )
type hostToServer map[string]models.Server type hostToServerData map[string]serverData
func (hts hostToServer) add(host, country, city string, tcp, udp bool) { type serverData struct {
server, ok := hts[host] openvpn bool
if !ok { wireguard bool
server.VPN = vpn.OpenVPN country string
server.Hostname = host city string
server.Country = country openvpnUDP bool
server.City = city openvpnTCP bool
ips []netip.Addr
}
func (hts hostToServerData) add(host, vpnType, country, city string, tcp, udp bool) {
serverData, ok := hts[host]
switch vpnType {
case vpn.OpenVPN:
serverData.openvpn = true
serverData.openvpnTCP = serverData.openvpnTCP || tcp
serverData.openvpnUDP = serverData.openvpnUDP || udp
case vpn.Wireguard:
serverData.wireguard = true
default:
panic("protocol not supported")
} }
if city != "" {
if !ok {
serverData.country = country
serverData.city = city
} else if city != "" {
// some servers are listed without the city although // some servers are listed without the city although
// they are also listed with the city described, so update // they are also listed with the city described, so update
// the city field. // the city field.
server.City = city serverData.city = city
} }
if tcp {
server.TCP = true hts[host] = serverData
}
if udp {
server.UDP = true
}
hts[host] = server
} }
func (hts hostToServer) toHostsSlice() (hosts []string) { func (hts hostToServerData) toHostsSlice() (hosts []string) {
hosts = make([]string, 0, len(hts)) hosts = make([]string, 0, len(hts))
for host := range hts { for host := range hts {
hosts = append(hosts, host) hosts = append(hosts, host)
@@ -40,23 +53,41 @@ func (hts hostToServer) toHostsSlice() (hosts []string) {
return hosts return hosts
} }
func (hts hostToServer) adaptWithIPs(hostToIPs map[string][]netip.Addr) { func (hts hostToServerData) adaptWithIPs(hostToIPs map[string][]netip.Addr) {
for host, IPs := range hostToIPs { for host, serverData := range hts {
server := hts[host] ips := hostToIPs[host]
server.IPs = IPs if len(ips) == 0 {
hts[host] = server
}
for host, server := range hts {
if len(server.IPs) == 0 {
delete(hts, host) delete(hts, host)
continue
} }
serverData.ips = ips
hts[host] = serverData
} }
} }
func (hts hostToServer) toServersSlice() (servers []models.Server) { func (hts hostToServerData) toServersSlice() (servers []models.Server) {
servers = make([]models.Server, 0, len(hts)) servers = make([]models.Server, 0, 2*len(hts)) //nolint:gomnd
for _, server := range hts { for hostname, serverData := range hts {
servers = append(servers, server) baseServer := models.Server{
Hostname: hostname,
Country: serverData.country,
City: serverData.city,
IPs: serverData.ips,
}
if serverData.openvpn {
openvpnServer := baseServer
openvpnServer.VPN = vpn.OpenVPN
openvpnServer.TCP = serverData.openvpnTCP
openvpnServer.UDP = serverData.openvpnUDP
servers = append(servers, openvpnServer)
}
if serverData.wireguard {
wireguardServer := baseServer
wireguardServer.VPN = vpn.Wireguard
const wireguardPublicKey = "658QxufMbjOTmB61Z7f+c7Rjg7oqWLnepTalqBERjF0="
wireguardServer.WgPubKey = wireguardPublicKey
servers = append(servers, wireguardServer)
}
} }
return servers return servers
} }

View File

@@ -5,14 +5,15 @@ import (
"fmt" "fmt"
"sort" "sort"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/provider/common" "github.com/qdm12/gluetun/internal/provider/common"
) )
func (u *Updater) FetchServers(ctx context.Context, minServers int) ( func (u *Updater) FetchServers(ctx context.Context, minServers int) (
servers []models.Server, err error) { servers []models.Server, err error) {
protocols := []string{"tcp", "udp"} protocols := []string{"ikev2", "tcp", "udp"}
hts := make(hostToServer) hts := make(hostToServerData)
for _, protocol := range protocols { for _, protocol := range protocols {
apiServers, err := fetchAPIServers(ctx, u.client, protocol) apiServers, err := fetchAPIServers(ctx, u.client, protocol)
@@ -20,17 +21,20 @@ func (u *Updater) FetchServers(ctx context.Context, minServers int) (
return nil, fmt.Errorf("fetching %s servers from API: %w", protocol, err) return nil, fmt.Errorf("fetching %s servers from API: %w", protocol, err)
} }
for _, apiServer := range apiServers { for _, apiServer := range apiServers {
// all hostnames from the protocols TCP, UDP and IKEV2 support Wireguard
// per https://github.com/qdm12/gluetun-wiki/issues/76#issuecomment-2125420536
const wgTCP, wgUDP = false, false // ignored
hts.add(apiServer.hostname, vpn.Wireguard, apiServer.country, apiServer.city, wgTCP, wgUDP)
tcp := protocol == "tcp" tcp := protocol == "tcp"
udp := protocol == "udp" udp := protocol == "udp"
hts.add(apiServer.hostname, apiServer.country, apiServer.city, tcp, udp) if !tcp && !udp { // not an OpenVPN protocol, for example ikev2
continue
}
hts.add(apiServer.hostname, vpn.OpenVPN, apiServer.country, apiServer.city, tcp, udp)
} }
} }
if len(hts) < minServers {
return nil, fmt.Errorf("%w: %d and expected at least %d",
common.ErrNotEnoughServers, len(hts), minServers)
}
hosts := hts.toHostsSlice() hosts := hts.toHostsSlice()
resolveSettings := parallelResolverSettings(hosts) resolveSettings := parallelResolverSettings(hosts)
hostToIPs, warnings, err := u.parallelResolver.Resolve(ctx, resolveSettings) hostToIPs, warnings, err := u.parallelResolver.Resolve(ctx, resolveSettings)
@@ -41,15 +45,15 @@ func (u *Updater) FetchServers(ctx context.Context, minServers int) (
return nil, err return nil, err
} }
if len(hostToIPs) < minServers {
return nil, fmt.Errorf("%w: %d and expected at least %d",
common.ErrNotEnoughServers, len(servers), minServers)
}
hts.adaptWithIPs(hostToIPs) hts.adaptWithIPs(hostToIPs)
servers = hts.toServersSlice() servers = hts.toServersSlice()
if len(servers) < minServers {
return nil, fmt.Errorf("%w: %d and expected at least %d",
common.ErrNotEnoughServers, len(servers), minServers)
}
sort.Sort(models.SortableServers(servers)) sort.Sort(models.SortableServers(servers))
return servers, nil return servers, nil

File diff suppressed because it is too large Load Diff