feat(surfshark): Wireguard support (#587)

This commit is contained in:
Quentin McGaw 2022-08-26 10:55:46 -04:00 committed by GitHub
parent 4ace99f318
commit 5989f29035
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 2792 additions and 526 deletions

View File

@ -61,7 +61,7 @@ Lightweight swiss-knife-like VPN client to multiple VPN service providers
- Supports: **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: **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 **Mullvad**, **Ivpn** and **Windscribe** - For **Mullvad**, **Ivpn**, **Surfshark** and **Windscribe**
- For **Torguard**, **VPN Unlimited** and **WeVPN** using [the custom provider](https://github.com/qdm12/gluetun/wiki/Custom-provider) - For **Torguard**, **VPN Unlimited** and **WeVPN** using [the custom provider](https://github.com/qdm12/gluetun/wiki/Custom-provider)
- For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun/wiki/Custom-provider) - For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun/wiki/Custom-provider)
- 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

@ -37,6 +37,7 @@ var (
ErrWireguardEndpointIPNotSet = errors.New("endpoint IP is not set") ErrWireguardEndpointIPNotSet = errors.New("endpoint IP is not set")
ErrWireguardEndpointPortNotAllowed = errors.New("endpoint port is not allowed") ErrWireguardEndpointPortNotAllowed = errors.New("endpoint port is not allowed")
ErrWireguardEndpointPortNotSet = errors.New("endpoint port is not set") ErrWireguardEndpointPortNotSet = errors.New("endpoint port is not set")
ErrWireguardEndpointPortSet = errors.New("endpoint port is set")
ErrWireguardInterfaceAddressNotSet = errors.New("interface address is not set") ErrWireguardInterfaceAddressNotSet = errors.New("interface address is not set")
ErrWireguardInterfaceNotValid = errors.New("interface name is not valid") ErrWireguardInterfaceNotValid = errors.New("interface name is not valid")
ErrWireguardPreSharedKeyNotSet = errors.New("pre-shared key is not set") ErrWireguardPreSharedKeyNotSet = errors.New("pre-shared key is not set")

View File

@ -33,6 +33,7 @@ func (p *Provider) validate(vpnType string, storage Storage) (err error) {
providers.Custom, providers.Custom,
providers.Ivpn, providers.Ivpn,
providers.Mullvad, providers.Mullvad,
providers.Surfshark,
providers.Windscribe, providers.Windscribe,
} }
} }

View File

@ -38,6 +38,7 @@ func (w Wireguard) validate(vpnProvider string) (err error) {
providers.Custom, providers.Custom,
providers.Ivpn, providers.Ivpn,
providers.Mullvad, providers.Mullvad,
providers.Surfshark,
providers.Windscribe, providers.Windscribe,
) { ) {
// do not validate for VPN provider not supporting Wireguard // do not validate for VPN provider not supporting Wireguard

View File

@ -19,7 +19,7 @@ type WireguardSelection struct {
// in the internal state. // in the internal state.
EndpointIP net.IP EndpointIP net.IP
// EndpointPort is a the server port to use for the VPN server. // EndpointPort is a the server port to use for the VPN server.
// It is optional for VPN providers IVPN, Mullvad // It is optional for VPN providers IVPN, Mullvad, Surfshark
// and Windscribe, and compulsory for the others. // and Windscribe, and compulsory for the others.
// When optional, it can be set to 0 to indicate not use // When optional, it can be set to 0 to indicate not use
// a custom endpoint port. It cannot be nil in the internal // a custom endpoint port. It cannot be nil in the internal
@ -36,7 +36,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.Ivpn, providers.Mullvad, providers.Windscribe: // endpoint IP addresses are baked in case providers.Ivpn, providers.Mullvad,
providers.Surfshark, providers.Windscribe:
// endpoint IP addresses are baked in
case providers.Custom: case providers.Custom:
if len(w.EndpointIP) == 0 { if len(w.EndpointIP) == 0 {
return ErrWireguardEndpointIPNotSet return ErrWireguardEndpointIPNotSet
@ -51,6 +53,11 @@ func (w WireguardSelection) validate(vpnProvider string) (err error) {
if *w.EndpointPort == 0 { if *w.EndpointPort == 0 {
return ErrWireguardEndpointPortNotSet return ErrWireguardEndpointPortNotSet
} }
// EndpointPort cannot be set
case providers.Surfshark:
if *w.EndpointPort != 0 {
return ErrWireguardEndpointPortSet
}
case providers.Ivpn, providers.Mullvad, providers.Windscribe: case providers.Ivpn, providers.Mullvad, providers.Windscribe:
// EndpointPort is optional and can be 0 // EndpointPort is optional and can be 0
if *w.EndpointPort == 0 { if *w.EndpointPort == 0 {
@ -78,7 +85,9 @@ func (w WireguardSelection) validate(vpnProvider string) (err error) {
// Validate PublicKey // Validate PublicKey
switch vpnProvider { switch vpnProvider {
case providers.Ivpn, providers.Mullvad, providers.Windscribe: // public keys are baked in case providers.Ivpn, providers.Mullvad,
providers.Surfshark, providers.Windscribe:
// public keys are baked in
case providers.Custom: case providers.Custom:
if w.PublicKey == "" { if w.PublicKey == "" {
return ErrWireguardPublicKeyNotSet return ErrWireguardPublicKeyNotSet

View File

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

View File

@ -10,9 +10,9 @@ import (
"github.com/qdm12/gluetun/internal/provider/surfshark/servers" "github.com/qdm12/gluetun/internal/provider/surfshark/servers"
) )
// Note: no multi-hop and some servers are missing from their API. // Note: no multi-hop and some OpenVPN servers are missing from their API.
func addServersFromAPI(ctx context.Context, client *http.Client, func addServersFromAPI(ctx context.Context, client *http.Client,
hts hostToServer) (err error) { hts hostToServers) (err error) {
data, err := fetchAPI(ctx, client) data, err := fetchAPI(ctx, client)
if err != nil { if err != nil {
return err return err
@ -21,12 +21,18 @@ func addServersFromAPI(ctx context.Context, client *http.Client,
locationData := servers.LocationData() locationData := servers.LocationData()
hostToLocation := hostToLocation(locationData) hostToLocation := hostToLocation(locationData)
const tcp, udp = true, true
for _, serverData := range data { for _, serverData := range data {
locationData := hostToLocation[serverData.Host] // TODO remove in v4 locationData := hostToLocation[serverData.Host] // TODO remove in v4
retroLoc := locationData.RetroLoc // empty string if the host has no retro-compatible region retroLoc := locationData.RetroLoc // empty string if the host has no retro-compatible region
hts.add(serverData.Host, serverData.Region, serverData.Country,
tcp, udp := true, true // OpenVPN servers from API supports both TCP and UDP
hts.addOpenVPN(serverData.Host, serverData.Region, serverData.Country,
serverData.Location, retroLoc, tcp, udp) serverData.Location, retroLoc, tcp, udp)
if serverData.PubKey != "" {
hts.addWireguard(serverData.Host, serverData.Region, serverData.Country,
serverData.Location, retroLoc, serverData.PubKey)
}
} }
return nil return nil
@ -41,6 +47,7 @@ type serverData struct {
Region string `json:"region"` Region string `json:"region"`
Country string `json:"country"` Country string `json:"country"`
Location string `json:"location"` Location string `json:"location"`
PubKey string `json:"pubKey"`
} }
func fetchAPI(ctx context.Context, client *http.Client) ( func fetchAPI(ctx context.Context, client *http.Client) (

View File

@ -18,10 +18,10 @@ func Test_addServersFromAPI(t *testing.T) {
t.Parallel() t.Parallel()
testCases := map[string]struct { testCases := map[string]struct {
hts hostToServer hts hostToServers
responseStatus int responseStatus int
responseBody io.ReadCloser responseBody io.ReadCloser
expected hostToServer expected hostToServers
err error err error
}{ }{
"fetch API error": { "fetch API error": {
@ -29,17 +29,18 @@ func Test_addServersFromAPI(t *testing.T) {
err: errors.New("HTTP status code not OK: 204 No Content"), err: errors.New("HTTP status code not OK: 204 No Content"),
}, },
"success": { "success": {
hts: hostToServer{ hts: hostToServers{
"existinghost": {Hostname: "existinghost"}, "existinghost": []models.Server{{Hostname: "existinghost"}},
}, },
responseStatus: http.StatusOK, responseStatus: http.StatusOK,
responseBody: io.NopCloser(strings.NewReader(`[ responseBody: io.NopCloser(strings.NewReader(`[
{"connectionName":"host1","region":"region1","country":"country1","location":"location1"}, {"connectionName":"host1","region":"region1","country":"country1","location":"location1"},
{"connectionName":"host1","region":"region1","country":"country1","location":"location1","pubkey":"pubKeyValue"},
{"connectionName":"host2","region":"region2","country":"country1","location":"location2"} {"connectionName":"host2","region":"region2","country":"country1","location":"location2"}
]`)), ]`)),
expected: map[string]models.Server{ expected: map[string][]models.Server{
"existinghost": {Hostname: "existinghost"}, "existinghost": {{Hostname: "existinghost"}},
"host1": { "host1": {{
VPN: vpn.OpenVPN, VPN: vpn.OpenVPN,
Region: "region1", Region: "region1",
Country: "country1", Country: "country1",
@ -47,8 +48,15 @@ func Test_addServersFromAPI(t *testing.T) {
Hostname: "host1", Hostname: "host1",
TCP: true, TCP: true,
UDP: true, UDP: true,
}, }, {
"host2": { VPN: vpn.Wireguard,
Region: "region1",
Country: "country1",
City: "location1",
Hostname: "host1",
WgPubKey: "pubKeyValue",
}},
"host2": {{
VPN: vpn.OpenVPN, VPN: vpn.OpenVPN,
Region: "region2", Region: "region2",
Country: "country1", Country: "country1",
@ -57,6 +65,7 @@ func Test_addServersFromAPI(t *testing.T) {
TCP: true, TCP: true,
UDP: true, UDP: true,
}}, }},
},
}, },
} }
for name, testCase := range testCases { for name, testCase := range testCases {

View File

@ -7,52 +7,94 @@ import (
"github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/models"
) )
type hostToServer map[string]models.Server type hostToServers map[string][]models.Server
func (hts hostToServer) add(host, region, country, city, retroLoc string, tcp, udp bool) { func (hts hostToServers) addOpenVPN(host, region, country, city,
server, ok := hts[host] retroLoc string, tcp, udp bool) {
if !ok { // Check for existing server for this host and OpenVPN.
server.VPN = vpn.OpenVPN servers := hts[host]
server.Hostname = host for i, existingServer := range servers {
server.Region = region if existingServer.Hostname != host ||
server.Country = country existingServer.VPN != vpn.OpenVPN {
server.City = city continue
server.RetroLoc = retroLoc }
// Update OpenVPN supported protocols and return
if !existingServer.TCP {
servers[i].TCP = tcp
}
if !existingServer.UDP {
servers[i].UDP = udp
}
return
} }
if tcp {
server.TCP = tcp server := models.Server{
VPN: vpn.OpenVPN,
Region: region,
Country: country,
City: city,
RetroLoc: retroLoc,
Hostname: host,
TCP: tcp,
UDP: udp,
} }
if udp { hts[host] = append(servers, server)
server.UDP = udp
}
hts[host] = server
} }
func (hts hostToServer) toHostsSlice() (hosts []string) { func (hts hostToServers) addWireguard(host, region, country, city, retroLoc,
hosts = make([]string, 0, len(hts)) wgPubKey string) {
// Check for existing server for this host and Wireguard.
servers := hts[host]
for _, existingServer := range servers {
if existingServer.Hostname == host &&
existingServer.VPN == vpn.Wireguard {
// No update necessary for Wireguard
return
}
}
server := models.Server{
VPN: vpn.Wireguard,
Region: region,
Country: country,
City: city,
RetroLoc: retroLoc,
Hostname: host,
WgPubKey: wgPubKey,
}
hts[host] = append(servers, server)
}
func (hts hostToServers) toHostsSlice() (hosts []string) {
const vpnServerTypes = 2 // OpenVPN + Wireguard
hosts = make([]string, 0, vpnServerTypes*len(hts))
for host := range hts { for host := range hts {
hosts = append(hosts, host) hosts = append(hosts, host)
} }
return hosts return hosts
} }
func (hts hostToServer) adaptWithIPs(hostToIPs map[string][]net.IP) { func (hts hostToServers) adaptWithIPs(hostToIPs map[string][]net.IP) {
for host, IPs := range hostToIPs { for host, IPs := range hostToIPs {
server := hts[host] servers := hts[host]
server.IPs = IPs for i := range servers {
hts[host] = server servers[i].IPs = IPs
}
hts[host] = servers
} }
for host, server := range hts { for host, servers := range hts {
if len(server.IPs) == 0 { if len(servers[0].IPs) == 0 {
delete(hts, host) delete(hts, host)
} }
} }
} }
func (hts hostToServer) toServersSlice() (servers []models.Server) { func (hts hostToServers) toServersSlice() (servers []models.Server) {
servers = make([]models.Server, 0, len(hts)) const vpnServerTypes = 2 // OpenVPN + Wireguard
for _, server := range hts { servers = make([]models.Server, 0, vpnServerTypes*len(hts))
servers = append(servers, server) for _, serversForHost := range hts {
servers = append(servers, serversForHost...)
} }
return servers return servers
} }

View File

@ -5,7 +5,7 @@ import (
) )
// getRemainingServers finds extra servers not found in the API or in the ZIP file. // getRemainingServers finds extra servers not found in the API or in the ZIP file.
func getRemainingServers(hts hostToServer) { func getRemainingServers(hts hostToServers) {
locationData := servers.LocationData() locationData := servers.LocationData()
hostnameToLocationLeft := hostToLocation(locationData) hostnameToLocationLeft := hostToLocation(locationData)
for _, hostnameDone := range hts.toHostsSlice() { for _, hostnameDone := range hts.toHostsSlice() {
@ -13,9 +13,9 @@ func getRemainingServers(hts hostToServer) {
} }
for hostname, locationData := range hostnameToLocationLeft { for hostname, locationData := range hostnameToLocationLeft {
// we assume the server supports TCP and UDP // we assume the OpenVPN server supports both TCP and UDP
const tcp, udp = true, true const tcp, udp = true, true
hts.add(hostname, locationData.Region, locationData.Country, hts.addOpenVPN(hostname, locationData.Region, locationData.Country,
locationData.City, locationData.RetroLoc, tcp, udp) locationData.City, locationData.RetroLoc, tcp, udp)
} }
} }

View File

@ -11,7 +11,7 @@ import (
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) {
hts := make(hostToServer) hts := make(hostToServers)
err = addServersFromAPI(ctx, u.client, hts) err = addServersFromAPI(ctx, u.client, hts)
if err != nil { if err != nil {
@ -37,14 +37,13 @@ func (u *Updater) FetchServers(ctx context.Context, minServers int) (
if err != nil { if err != nil {
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)
if len(hts) < minServers {
return nil, fmt.Errorf("%w: %d and expected at least %d",
common.ErrNotEnoughServers, len(hts), minServers)
}
servers = hts.toServersSlice() servers = hts.toServersSlice()
sort.Sort(models.SortableServers(servers)) sort.Sort(models.SortableServers(servers))

View File

@ -10,7 +10,7 @@ import (
) )
func addOpenVPNServersFromZip(ctx context.Context, func addOpenVPNServersFromZip(ctx context.Context,
unzipper common.Unzipper, hts hostToServer) ( unzipper common.Unzipper, hts hostToServers) (
warnings []string, err error) { warnings []string, err error) {
const url = "https://my.surfshark.com/vpn/api/v1/server/configurations" const url = "https://my.surfshark.com/vpn/api/v1/server/configurations"
contents, err := unzipper.FetchAndExtract(ctx, url) contents, err := unzipper.FetchAndExtract(ctx, url)
@ -66,7 +66,7 @@ func addOpenVPNServersFromZip(ctx context.Context,
continue continue
} }
hts.add(host, data.Region, data.Country, data.City, hts.addOpenVPN(host, data.Region, data.Country, data.City,
data.RetroLoc, tcp, udp) data.RetroLoc, tcp, udp)
} }

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,7 @@
- gofumpt - gofumpt
- Use netip - Use netip
- Split servers.json - Split servers.json
- Common slice of Wireguard providers in config settings
- DNS block lists as LFS and built in image - DNS block lists as LFS and built in image
- Add HTTP server v3 as json rpc - Add HTTP server v3 as json rpc
- Use `github.com/qdm12/ddns-updater/pkg/publicip` - Use `github.com/qdm12/ddns-updater/pkg/publicip`