Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(nordvpn): new API endpoint and wireguard support #1380

Merged
merged 3 commits into from
Jun 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions internal/configuration/settings/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func (p *Provider) validate(vpnType string, storage Storage) (err error) {
providers.Custom,
providers.Ivpn,
providers.Mullvad,
providers.Nordvpn,
providers.Surfshark,
providers.Windscribe,
}
Expand Down
2 changes: 1 addition & 1 deletion internal/configuration/settings/vpn.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func (v *VPN) setDefaults() {
v.Type = gosettings.DefaultString(v.Type, vpn.OpenVPN)
v.Provider.setDefaults()
v.OpenVPN.setDefaults(*v.Provider.Name)
v.Wireguard.setDefaults()
v.Wireguard.setDefaults(*v.Provider.Name)
}

func (v VPN) String() string {
Expand Down
8 changes: 7 additions & 1 deletion internal/configuration/settings/wireguard.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ func (w Wireguard) validate(vpnProvider string, ipv6Supported bool) (err error)
providers.Custom,
providers.Ivpn,
providers.Mullvad,
providers.Nordvpn,
providers.Surfshark,
providers.Windscribe,
) {
Expand Down Expand Up @@ -140,9 +141,14 @@ func (w *Wireguard) overrideWith(other Wireguard) {
w.Implementation = gosettings.OverrideWithString(w.Implementation, other.Implementation)
}

func (w *Wireguard) setDefaults() {
func (w *Wireguard) setDefaults(vpnProvider string) {
w.PrivateKey = gosettings.DefaultPointer(w.PrivateKey, "")
w.PreSharedKey = gosettings.DefaultPointer(w.PreSharedKey, "")
if vpnProvider == providers.Nordvpn {
defaultNordVPNAddress := netip.AddrFrom4([4]byte{10, 5, 0, 2})
defaultNordVPNPrefix := netip.PrefixFrom(defaultNordVPNAddress, defaultNordVPNAddress.BitLen())
w.Addresses = gosettings.DefaultSlice(w.Addresses, []netip.Prefix{defaultNordVPNPrefix})
}
w.Interface = gosettings.DefaultString(w.Interface, "wg0")
w.MTU = gosettings.DefaultNumber(w.MTU, wireguarddevice.DefaultMTU)
w.Implementation = gosettings.DefaultString(w.Implementation, "auto")
Expand Down
4 changes: 2 additions & 2 deletions internal/configuration/settings/wireguardselection.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func (w WireguardSelection) validate(vpnProvider string) (err error) {
// Validate EndpointIP
switch vpnProvider {
case providers.Airvpn, providers.Ivpn, providers.Mullvad,
providers.Surfshark, providers.Windscribe:
providers.Nordvpn, providers.Surfshark, providers.Windscribe:
// endpoint IP addresses are baked in
case providers.Custom:
if !w.EndpointIP.IsValid() || w.EndpointIP.IsUnspecified() {
Expand All @@ -55,7 +55,7 @@ func (w WireguardSelection) validate(vpnProvider string) (err error) {
return fmt.Errorf("%w", ErrWireguardEndpointPortNotSet)
}
// EndpointPort cannot be set
case providers.Surfshark:
case providers.Surfshark, providers.Nordvpn:
if *w.EndpointPort != 0 {
return fmt.Errorf("%w", ErrWireguardEndpointPortSet)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/provider/nordvpn/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (

func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) (
connection models.Connection, err error) {
defaults := utils.NewConnectionDefaults(443, 1194, 0) //nolint:gomnd
defaults := utils.NewConnectionDefaults(443, 1194, 51820) //nolint:gomnd
qdm12 marked this conversation as resolved.
Show resolved Hide resolved
return utils.GetConnection(p.Name(),
p.storage, selection, defaults, ipv6Supported, p.randSource)
}
20 changes: 7 additions & 13 deletions internal/provider/nordvpn/updater/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,13 @@ var (
ErrHTTPStatusCodeNotOK = errors.New("HTTP status code not OK")
)

type serverData struct {
Domain string `json:"domain"`
IPAddress string `json:"ip_address"`
Name string `json:"name"`
Country string `json:"country"`
Features struct {
UDP bool `json:"openvpn_udp"`
TCP bool `json:"openvpn_tcp"`
} `json:"features"`
}

func fetchAPI(ctx context.Context, client *http.Client) (data []serverData, err error) {
const url = "https://nordvpn.com/api/server"
func fetchAPI(ctx context.Context, client *http.Client,
recommended bool, limit uint) (data []serverData, err error) {
url := "https://api.nordvpn.com/v1/servers/"
if recommended {
url += "recommendations"
}
url += fmt.Sprintf("?limit=%d", limit) // 0 means no limit

request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
Expand Down
17 changes: 0 additions & 17 deletions internal/provider/nordvpn/updater/ip.go

This file was deleted.

131 changes: 131 additions & 0 deletions internal/provider/nordvpn/updater/models.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package updater

import (
"encoding/base64"
"errors"
"fmt"
"net/netip"
)

// Check out the JSON data from https://api.nordvpn.com/v1/servers?limit=10
type serverData struct {
// Name is the server name, for example 'Poland #128'
Name string `json:"name"`
// Stations is, it seems, the entry IP address.
// However it is ignored in favor of the 'ips' entry field.
Station netip.Addr `json:"station"`
// IPv6Station is mostly empty, so we ignore it for now.
IPv6Station netip.Addr `json:"station_ipv6"`
// Hostname is the server hostname, for example 'pl128.nordvpn.com'
Hostname string
// Status is the server status, for example 'online'
Status string `json:"status"`
// Locations is the list of locations for the server.
// Only the first location is taken into account for now.
Locations []struct {
Country struct {
// Name is the country name, for example 'Poland'.
Name string `json:"name"`
City struct {
// Name is the city name, for example 'Warsaw'.
Name string `json:"name"`
} `json:"city"`
} `json:"country"`
} `json:"locations"`
Technologies []struct {
// Identifier is the technology id name, it can notably be:
// - openvpn_udp
// - openvpn_tcp
// - wireguard_udp
Identifier string `json:"identifier"`
// Metadata is notably useful for the Wireguard public key.
Metadata []struct {
// Name can notably be 'public_key'.
Name string `json:"name"`
// Value can notably the Wireguard public key value.
Value string `json:"value"`
} `json:"metadata"`
} `json:"technologies"`
Groups []struct {
// Title can notably be the region name, for example 'Europe',
// if the group's type/identifier is 'regions'.
Title string `json:"title"`
Type struct {
// Identifier can be 'regions'.
Identifier string `json:"identifier"`
} `json:"type"`
} `json:"groups"`
// IPs is the list of IP addresses for the server.
IPs []struct {
// Type can notably be 'entry'.
Type string `json:"type"`
IP struct {
IP netip.Addr `json:"ip"`
} `json:"ip"`
} `json:"ips"`
}

// country returns the country name of the server.
func (s *serverData) country() (country string) {
if len(s.Locations) == 0 {
return ""
}
return s.Locations[0].Country.Name
}

// region returns the region name of the server.
func (s *serverData) region() (region string) {
for _, group := range s.Groups {
if group.Type.Identifier == "regions" {
return group.Title
}
}
return ""
}

// city returns the city name of the server.
func (s *serverData) city() (city string) {
if len(s.Locations) == 0 {
return ""
}
return s.Locations[0].Country.City.Name
}

// ips returns the list of IP addresses for the server.
func (s *serverData) ips() (ips []netip.Addr) {
ips = make([]netip.Addr, 0, len(s.IPs))
for _, ipObject := range s.IPs {
if ipObject.Type != "entry" {
continue
}
ips = append(ips, ipObject.IP.IP)
}
return ips
}

var (
ErrWireguardPublicKeyMalformed = errors.New("wireguard public key is malformed")
ErrWireguardPublicKeyNotFound = errors.New("wireguard public key not found")
)

// wireguardPublicKey returns the Wireguard public key for the server.
func (s *serverData) wireguardPublicKey() (wgPubKey string, err error) {
for _, technology := range s.Technologies {
if technology.Identifier != "wireguard_udp" {
continue
}
for _, metadata := range technology.Metadata {
if metadata.Name != "public_key" {
continue
}
wgPubKey = metadata.Value
_, err = base64.StdEncoding.DecodeString(wgPubKey)
if err != nil {
return "", fmt.Errorf("%w: %s cannot be decoded: %s",
ErrWireguardPublicKeyMalformed, wgPubKey, err)
}
return metadata.Value, nil
}
}
return "", fmt.Errorf("%w", ErrWireguardPublicKeyNotFound)
}
70 changes: 52 additions & 18 deletions internal/provider/nordvpn/updater/servers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"net/netip"
"sort"

"github.com/qdm12/gluetun/internal/constants/vpn"
Expand All @@ -18,39 +17,74 @@ var (

func (u *Updater) FetchServers(ctx context.Context, minServers int) (
servers []models.Server, err error) {
data, err := fetchAPI(ctx, u.client)
const recommended = true
const limit = 0
data, err := fetchAPI(ctx, u.client, recommended, limit)
if err != nil {
return nil, err
}

servers = make([]models.Server, 0, len(data))

for _, jsonServer := range data {
if !jsonServer.Features.TCP && !jsonServer.Features.UDP {
u.warner.Warn("server does not support TCP and UDP for openvpn: " + jsonServer.Name)
if jsonServer.Status != "online" {
u.warner.Warn(fmt.Sprintf("ignoring offline server %s", jsonServer.Name))
continue
}

ip, err := parseIPv4(jsonServer.IPAddress)
if err != nil {
return nil, fmt.Errorf("%w for server %s", err, jsonServer.Name)
server := models.Server{
Country: jsonServer.country(),
Region: jsonServer.region(),
City: jsonServer.city(),
Hostname: jsonServer.Hostname,
IPs: jsonServer.ips(),
}

number, err := parseServerName(jsonServer.Name)
if err != nil {
return nil, err
switch {
case errors.Is(err, ErrNoIDInServerName):
u.warner.Warn(fmt.Sprintf("%s - leaving server number as 0", err))
case err != nil:
u.warner.Warn(fmt.Sprintf("failed parsing server name: %s", err))
continue
default: // no error
server.Number = number
}

server := models.Server{
VPN: vpn.OpenVPN,
Region: jsonServer.Country,
Hostname: jsonServer.Domain,
Number: number,
IPs: []netip.Addr{ip},
TCP: jsonServer.Features.TCP,
UDP: jsonServer.Features.UDP,
var wireguardFound, openvpnFound bool
wireguardServer := server
wireguardServer.VPN = vpn.Wireguard
openVPNServer := server // accumulate UDP+TCP technologies
openVPNServer.VPN = vpn.OpenVPN

for _, technology := range jsonServer.Technologies {
switch technology.Identifier {
case "openvpn_udp":
openvpnFound = true
openVPNServer.UDP = true
case "openvpn_tcp":
openvpnFound = true
openVPNServer.TCP = true
case "wireguard_udp":
wireguardFound = true
wireguardServer.WgPubKey, err = jsonServer.wireguardPublicKey()
if err != nil {
u.warner.Warn(fmt.Sprintf("ignoring Wireguard server %s: %s",
jsonServer.Name, err))
wireguardFound = false
continue
}
default: // Ignore other technologies
continue
}
}

if openvpnFound {
servers = append(servers, openVPNServer)
}
if wireguardFound {
servers = append(servers, wireguardServer)
}
servers = append(servers, server)
}

if len(servers) < minServers {
Expand Down
Loading
Loading