Nordvpn support (#189), fix #178

This commit is contained in:
Quentin McGaw
2020-07-15 18:14:45 -04:00
committed by GitHub
parent 616ba0c538
commit 1281026850
15 changed files with 5564 additions and 51 deletions

View File

@@ -42,7 +42,7 @@ ENV VPNSP=pia \
UID=1000 \
GID=1000 \
IP_STATUS_FILE="/ip" \
# PIA, Windscribe, Surfshark and Cyberghost only
# PIA, Windscribe, Surfshark, Cyberghost, Vyprvpn, NordVPN only
USER= \
PASSWORD= \
REGION="Austria" \
@@ -58,6 +58,8 @@ ENV VPNSP=pia \
PORT= \
# Cyberghost only
CYBERGHOST_GROUP="Premium UDP Europe" \
# NordVPN only
SERVER_NUMBER= \
# Openvpn
OPENVPN_CIPHER= \
OPENVPN_AUTH= \

View File

@@ -1,6 +1,8 @@
# Gluetun VPN client
*Lightweight swiss-knife-like VPN client to tunnel to Private Internet Access, Mullvad, Windscribe, Surfshark and Cyberghost VPN servers, using Go, OpenVPN, iptables, DNS over TLS, ShadowSocks and Tinyproxy*
*Lightweight swiss-knife-like VPN client to tunnel to Private Internet Access,
Mullvad, Windscribe, Surfshark Cyberghost and NordVPN VPN servers, using Go, OpenVPN,
iptables, DNS over TLS, ShadowSocks and Tinyproxy*
**ANNOUNCEMENT**: *[Video of the Git history of Gluetun](https://youtu.be/khipOYJtGJ0)*
@@ -33,7 +35,8 @@
## Features
- Based on Alpine 3.12 for a small Docker image of 52MB
- Supports **Private Internet Access**, **Mullvad**, **Windscribe**, **Surfshark** and **Cyberghost** servers
- Supports **Private Internet Access**, **Mullvad**, **Windscribe**,
**Surfshark**, **Cyberghost** and **NordVPN** servers
- 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
- Choose the vpn network protocol, `udp` or `tcp`
@@ -90,9 +93,9 @@
[![https://windscribe.com/?affid=mh7nyafu](https://raw.githubusercontent.com/qdm12/private-internet-access-docker/master/doc/windscribe.jpg)](https://windscribe.com/?affid=mh7nyafu)
- Surfshark: **username** and **password** ([sign up](https://order.surfshark.com/))
- Cyberghost: **username**, **password** and **device client key file**
([sign up](https://www.cyberghostvpn.com/en_US/buy/cyberghost-vpn-4))
- Cyberghost: **username**, **password** and **device client key file** ([sign up](https://www.cyberghostvpn.com/en_US/buy/cyberghost-vpn-4))
- Vyprvpn: **username** and **password**
- NordVPN: **username** and **password**
- If you have a host or router firewall, please refer [to the firewall documentation](https://github.com/qdm12/private-internet-access-docker/blob/master/doc/firewall.md)
1. On some devices you may need to setup your tunnel kernel module on your host with `insmod /lib/modules/tun.ko` or `modprobe tun`
@@ -118,7 +121,6 @@
- Use `-p 8888:8888/tcp` to access the HTTP web proxy (and put your LAN in `EXTRA_SUBNETS` environment variable, in example `192.168.1.0/24`)
- Use `-p 8388:8388/tcp -p 8388:8388/udp` to access the SOCKS5 proxy (and put your LAN in `EXTRA_SUBNETS` environment variable, in example `192.168.1.0/24`)
- Use `-p 8000:8000/tcp` to access the [HTTP control server](#HTTP-control-server) built-in
- Pass additional arguments to *openvpn* using Docker's command function (commands after the image name)
**If you encounter an issue with the tun device not being available, see [the FAQ](https://github.com/qdm12/private-internet-access-docker/blob/master/doc/faq.md#how-to-fix-openvpn-failing-to-start)**
@@ -142,7 +144,7 @@ Want more testing? ▶ [see the Wiki](https://github.com/qdm12/private-internet-
| Variable | Default | Choices | Description |
| --- | --- | --- | --- |
| 🏁 `VPNSP` | `private internet access` | `private internet access`, `mullvad`, `windscribe`, `surfshark`, `vyprvpn` | VPN Service Provider |
| 🏁 `VPNSP` | `private internet access` | `private internet access`, `mullvad`, `windscribe`, `surfshark`, `vyprvpn`, `nordvpn` | VPN Service Provider |
| `IP_STATUS_FILE` | `/ip` | Any filepath | Filepath to store the public IP address assigned |
| `PROTOCOL` | `udp` | `udp` or `tcp` | Network protocol to use |
| `OPENVPN_VERBOSITY` | `1` | `0` to `6` | Openvpn verbosity level |
@@ -210,13 +212,22 @@ Want more testing? ▶ [see the Wiki](https://github.com/qdm12/private-internet-
And use the line produced as the value for the environment variable `CLIENT_KEY`.
- VyprVPN
- NordVPN
| Variable | Default | Choices | Description |
| --- | --- | --- | --- |
| 🏁 `USER` | | | Your username |
| 🏁 `PASSWORD` | | | Your password |
| `REGION` | `Austria` | One of the [VyprVPN regions](https://www.vyprvpn.com/server-locations) | VPN server region |
| `SERVER_NUMBER` | | Server integer number | Optional server number. For example `251` for `Italy #251` |
- NordVPN
| Variable | Default | Choices | Description |
| --- | --- | --- | --- |
| 🏁 `USER` | | | Your username |
| 🏁 `PASSWORD` | | | Your password |
| 🏁 `REGION` | `Austria` (wrong) | One of the NordVPN server name, i.e. `Cyprus #12` | VPN server name |
### DNS over TLS

106
cmd/mapper/main.go Normal file
View File

@@ -0,0 +1,106 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"net"
"net/http"
"os"
"sort"
"strconv"
"strings"
"time"
"github.com/qdm12/golibs/network"
"github.com/qdm12/private-internet-access-docker/internal/models"
)
func main() {
os.Exit(_main())
}
func _main() int {
provider := flag.String("provider", "nordvpn", "VPN provider to map region to IP addresses using their API, can be 'nordvpn'")
flag.Parse()
client := network.NewClient(30 * time.Second) // big file so 30 seconds
switch *provider {
case "nordvpn":
servers, ignoredServers, err := nordvpn(client)
if err != nil {
fmt.Println(err)
return 1
}
for _, server := range servers {
fmt.Printf(
"{Region: %q, Number: %d, TCP: %t, UDP: %t, IP: net.IP{%s}},\n",
server.Region, server.Number, server.TCP, server.UDP, strings.ReplaceAll(server.IP.String(), ".", ", "),
)
}
fmt.Print("\n\n")
for _, serverName := range ignoredServers {
fmt.Printf("ignored server %q because it does not support both UDP and TCP\n", serverName)
}
default:
fmt.Printf("Provider %q is not supported\n", *provider)
return 1
}
return 0
}
func nordvpn(client network.Client) (servers []models.NordvpnServer, ignoredServers []string, err error) {
content, status, err := client.GetContent("https://nordvpn.com/api/server")
if err != nil {
return nil, nil, err
} else if status != http.StatusOK {
return nil, nil, fmt.Errorf("HTTP status %d from NordVPN API", status)
}
response := []struct {
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"`
}{}
if err := json.Unmarshal(content, &response); err != nil {
return nil, nil, err
}
for _, element := range response {
if !element.Features.TCP && !element.Features.UDP {
ignoredServers = append(ignoredServers, element.Name)
}
ip := net.ParseIP(element.IPAddress)
if ip == nil {
return nil, nil, fmt.Errorf("IP address %q is not valid for server %q", element.IPAddress, element.Name)
}
i := strings.IndexRune(element.Name, '#')
if i < 0 {
return nil, nil, fmt.Errorf("No ID in server name %q", element.Name)
}
idString := element.Name[i+1:]
idUint64, err := strconv.ParseUint(idString, 10, 16)
if err != nil {
return nil, nil, fmt.Errorf("Bad ID in server name %q", element.Name)
}
id := uint16(idUint64)
server := models.NordvpnServer{
Region: element.Country,
Number: id,
IP: ip,
TCP: element.Features.TCP,
UDP: element.Features.UDP,
}
servers = append(servers, server)
}
sort.Slice(servers, func(i, j int) bool {
if servers[i].Region == servers[j].Region {
return servers[i].Number < servers[j].Number
}
return servers[i].Region < servers[j].Region
})
return servers, ignoredServers, nil
}

View File

@@ -15,50 +15,25 @@ services:
environment:
# More variables are available, see the readme table
- VPNSP=private internet access
- PROTOCOL=udp
- OPENVPN_VERBOSITY=1
- OPENVPN_ROOT=no
- OPENVPN_TARGET_IP=
# Timezone for accurate logs times
- TZ=
# PIA, Windscribe, Surfshark and Cyberghost only
- REGION=Austria
# All VPN providers
- USER=js89ds7
# All VPN providers but Mullvad
- PASSWORD=8fd9s239G
# PIA only
- PIA_ENCRYPTION=strong
- PORT_FORWARDING=off
# Cyberghost only
- CLIENT_KEY=
# All VPN providers but Mullvad
- REGION=Austria
# Mullvad only
- COUNTRY=Sweden
- CITY=
- ISP=
# Mullvad and Windscribe only
- PORT=
# Cyberghost only
- CYBERGHOST_GROUP=Premium UDP Europe
- CLIENT_KEY=
# DNS over TLS
- DOT=on
- DOT_PROVIDERS=cloudflare
- DOT_IPV6=off
- DOT_VERBOSITY=1
- BLOCK_MALICIOUS=on
- BLOCK_SURVEILLANCE=off
- BLOCK_ADS=off
- UNBLOCK=
- DNS_UPDATE_PERIOD=24h
# Firewall
# Allow for example your LAN, set to: 192.168.1.0/24
- EXTRA_SUBNETS=
# Shadowsocks
- SHADOWSOCKS=off
- SHADOWSOCKS_PASSWORD=
# Tinyproxy
- TINYPROXY=off
- TINYPROXY_USER=
- TINYPROXY_PASSWORD=
restart: always

File diff suppressed because it is too large Load Diff

View File

@@ -17,6 +17,8 @@ const (
Cyberghost models.VPNProvider = "cyberghost"
// Vyprvpn is a VPN provider
Vyprvpn models.VPNProvider = "vyprvpn"
// NordVPN is a VPN provider
Nordvpn models.VPNProvider = "nordvpn"
)
const (

View File

@@ -14,12 +14,12 @@ type ProviderSettings struct {
PortForwarding PortForwarding
}
type ServerSelection struct {
type ServerSelection struct { //nolint:maligned
// Common
Protocol NetworkProtocol
TargetIP net.IP
// Cyberghost, PIA, Surfshark, Windscribe
// Cyberghost, PIA, Surfshark, Windscribe, Vyprvpn, NordVPN
Region string
// Cyberghost
@@ -36,6 +36,9 @@ type ServerSelection struct {
// PIA
EncryptionPreset string
// NordVPN
Number uint16
}
type ExtraConfigOptions struct {
@@ -61,6 +64,14 @@ func (p *ProviderSettings) String() string {
fmt.Sprintf("%s settings:", strings.Title(string(p.Name))),
"Network protocol: " + string(p.ServerSelection.Protocol),
}
customPort := ""
if p.ServerSelection.CustomPort > 0 {
customPort = fmt.Sprintf("%d", p.ServerSelection.CustomPort)
}
number := ""
if p.ServerSelection.Number > 0 {
number = fmt.Sprintf("%d", p.ServerSelection.Number)
}
switch strings.ToLower(string(p.Name)) {
case "private internet access":
settingsList = append(settingsList,
@@ -73,12 +84,12 @@ func (p *ProviderSettings) String() string {
"Country: "+p.ServerSelection.Country,
"City: "+p.ServerSelection.City,
"ISP: "+p.ServerSelection.ISP,
"Custom port: "+string(p.ServerSelection.CustomPort),
"Custom port: "+customPort,
)
case "windscribe":
settingsList = append(settingsList,
"Region: "+p.ServerSelection.Region,
"Custom port: "+string(p.ServerSelection.CustomPort),
"Custom port: "+customPort,
)
case "surfshark":
settingsList = append(settingsList,
@@ -94,6 +105,15 @@ func (p *ProviderSettings) String() string {
settingsList = append(settingsList,
"Region: "+p.ServerSelection.Region,
)
case "nordvpn":
settingsList = append(settingsList,
"Region: "+p.ServerSelection.Region,
"Number: "+number,
)
default:
settingsList = append(settingsList,
"<Missing String method, please implement me!>",
)
}
if p.ServerSelection.TargetIP != nil {
settingsList = append(settingsList,

View File

@@ -36,3 +36,11 @@ type VyprvpnServer struct {
Region string
IPs []net.IP
}
type NordvpnServer struct { //nolint:maligned
Region string
Number uint16
IP net.IP
TCP bool
UDP bool
}

View File

@@ -0,0 +1,22 @@
package params
import (
libparams "github.com/qdm12/golibs/params"
"github.com/qdm12/private-internet-access-docker/internal/constants"
)
// GetNordvpnRegion obtains the region (country) for the NordVPN server from the
// environment variable REGION
func (r *reader) GetNordvpnRegion() (region string, err error) {
return r.envParams.GetValueIfInside("REGION", constants.NordvpnRegionChoices())
}
// GetNordvpnRegion obtains the server number (optional) for the NordVPN server from the
// environment variable SERVER_NUMBER
func (r *reader) GetNordvpnNumber() (number uint16, err error) {
n, err := r.envParams.GetEnvIntRange("SERVER_NUMBER", 0, 65535, libparams.Default("0"))
if err != nil {
return 0, err
}
return uint16(n), nil
}

View File

@@ -81,6 +81,10 @@ type Reader interface {
// Vyprvpn getters
GetVyprvpnRegion() (region string, err error)
// NordVPN getters
GetNordvpnRegion() (region string, err error)
GetNordvpnNumber() (number uint16, err error)
// Shadowsocks getters
GetShadowSocks() (activated bool, err error)
GetShadowSocksLog() (activated bool, err error)
@@ -123,7 +127,7 @@ func NewReader(logger logging.Logger, fileManager files.FileManager) Reader {
// GetVPNSP obtains the VPN service provider to use from the environment variable VPNSP
func (r *reader) GetVPNSP() (vpnServiceProvider models.VPNProvider, err error) {
s, err := r.envParams.GetValueIfInside("VPNSP", []string{"pia", "private internet access", "mullvad", "windscribe", "surfshark", "cyberghost", "vyprvpn"})
s, err := r.envParams.GetValueIfInside("VPNSP", []string{"pia", "private internet access", "mullvad", "windscribe", "surfshark", "cyberghost", "vyprvpn", "nordvpn"})
if s == "pia" {
s = "private internet access"
}

View File

@@ -0,0 +1,153 @@
package provider
import (
"fmt"
"net"
"strings"
"github.com/qdm12/golibs/network"
"github.com/qdm12/private-internet-access-docker/internal/constants"
"github.com/qdm12/private-internet-access-docker/internal/models"
)
type nordvpn struct{}
func newNordvpn() *nordvpn {
return &nordvpn{}
}
func findServers(selection models.ServerSelection) (servers []models.NordvpnServer) {
for _, server := range constants.NordvpnServers() {
if strings.EqualFold(server.Region, selection.Region) {
if (selection.Protocol == constants.TCP && !server.TCP) || (selection.Protocol == constants.UDP && !server.UDP) {
continue
}
if selection.Number > 0 && server.Number == selection.Number {
return []models.NordvpnServer{server}
}
servers = append(servers, server)
}
}
return servers
}
func extractIPsFromServers(servers []models.NordvpnServer) (ips []net.IP) {
ips = make([]net.IP, len(servers))
for i := range servers {
ips[i] = servers[i].IP
}
return ips
}
func targetIPInIps(targetIP net.IP, ips []net.IP) error {
for i := range ips {
if targetIP.Equal(ips[i]) {
return nil
}
}
ipsString := make([]string, len(ips))
for i := range ips {
ipsString[i] = ips[i].String()
}
return fmt.Errorf("target IP address %s not found in IP addresses %s", targetIP, strings.Join(ipsString, ", "))
}
func (n *nordvpn) GetOpenVPNConnections(selection models.ServerSelection) (connections []models.OpenVPNConnection, err error) { //nolint:dupl
servers := findServers(selection)
ips := extractIPsFromServers(servers)
if len(ips) == 0 {
if selection.Number > 0 {
return nil, fmt.Errorf("no IP found for region %q, protocol %s and number %d", selection.Region, selection.Protocol, selection.Number)
}
return nil, fmt.Errorf("no IP found for region %q, protocol %s", selection.Region, selection.Protocol)
}
var IP net.IP
if selection.TargetIP != nil {
if err := targetIPInIps(selection.TargetIP, ips); err != nil {
return nil, err
}
IP = selection.TargetIP
} else {
IP = ips[0]
}
var port uint16
switch {
case selection.Protocol == constants.UDP:
port = 1194
case selection.Protocol == constants.TCP:
port = 443
default:
return nil, fmt.Errorf("protocol %q is unknown", selection.Protocol)
}
return []models.OpenVPNConnection{{IP: IP, Port: port, Protocol: selection.Protocol}}, nil
}
func (n *nordvpn) BuildConf(connections []models.OpenVPNConnection, verbosity, uid, gid int, root bool, cipher, auth string, extras models.ExtraConfigOptions) (lines []string) { //nolint:dupl
if len(cipher) == 0 {
cipher = aes256cbc
}
if len(auth) == 0 {
auth = "sha512"
}
lines = []string{
"client",
"dev tun",
"nobind",
"persist-key",
"remote-cert-tls server",
// Nordvpn specific
"resolv-retry infinite",
"tun-mtu 1500",
"tun-mtu-extra 32",
"mssfix 1450",
"ping 15",
"ping-restart 0",
"ping-timer-rem",
"reneg-sec 0",
"comp-lzo no",
"fast-io",
"key-direction 1",
// Added constant values
"auth-nocache",
"mute-replay-warnings",
"pull-filter ignore \"auth-token\"", // prevent auth failed loops
"auth-retry nointeract",
"remote-random",
"suppress-timestamps",
// Modified variables
fmt.Sprintf("verb %d", verbosity),
fmt.Sprintf("auth-user-pass %s", constants.OpenVPNAuthConf),
fmt.Sprintf("proto %s", string(connections[0].Protocol)),
fmt.Sprintf("cipher %s", cipher),
fmt.Sprintf("auth %s", auth),
}
if !root {
lines = append(lines, "user nonrootuser")
}
for _, connection := range connections {
lines = append(lines, fmt.Sprintf("remote %s %d", connection.IP.String(), connection.Port))
}
lines = append(lines, []string{
"<ca>",
"-----BEGIN CERTIFICATE-----",
constants.NordvpnCertificate,
"-----END CERTIFICATE-----",
"</ca>",
}...)
lines = append(lines, []string{
"<tls-auth>",
"-----BEGIN OpenVPN Static key V1-----",
constants.NordvpnOpenvpnStaticKeyV1,
"-----END OpenVPN Static key V1-----",
"</tls-auth>",
"",
}...)
return lines
}
func (n *nordvpn) GetPortForward(client network.Client) (port uint16, err error) {
panic("port forwarding is not supported for nordvpn")
}

View File

@@ -27,6 +27,8 @@ func New(provider models.VPNProvider) Provider {
return newCyberghost()
case constants.Vyprvpn:
return newVyprvpn()
case constants.Nordvpn:
return newNordvpn()
default:
return nil // should never occur
}

View File

@@ -16,7 +16,7 @@ func newSurfshark() *surfshark {
return &surfshark{}
}
func (s *surfshark) GetOpenVPNConnections(selection models.ServerSelection) (connections []models.OpenVPNConnection, err error) {
func (s *surfshark) GetOpenVPNConnections(selection models.ServerSelection) (connections []models.OpenVPNConnection, err error) { //nolint:dupl
var IPs []net.IP
for _, server := range constants.SurfsharkServers() {
if strings.EqualFold(server.Region, selection.Region) {
@@ -54,7 +54,7 @@ func (s *surfshark) GetOpenVPNConnections(selection models.ServerSelection) (con
return connections, nil
}
func (s *surfshark) BuildConf(connections []models.OpenVPNConnection, verbosity, uid, gid int, root bool, cipher, auth string, extras models.ExtraConfigOptions) (lines []string) {
func (s *surfshark) BuildConf(connections []models.OpenVPNConnection, verbosity, uid, gid int, root bool, cipher, auth string, extras models.ExtraConfigOptions) (lines []string) { //nolint:dupl
if len(cipher) == 0 {
cipher = aes256cbc
}

View File

@@ -64,6 +64,8 @@ func GetOpenVPNSettings(paramsReader params.Reader, vpnProvider models.VPNProvid
settings.Provider, err = GetCyberghostSettings(paramsReader)
case constants.Vyprvpn:
settings.Provider, err = GetVyprvpnSettings(paramsReader)
case constants.Nordvpn:
settings.Provider, err = GetNordvpnSettings(paramsReader)
default:
err = fmt.Errorf("VPN service provider %q is not valid", vpnProvider)
}

View File

@@ -153,3 +153,25 @@ func GetVyprvpnSettings(paramsReader params.Reader) (settings models.ProviderSet
}
return settings, nil
}
// GetNordvpnSettings obtains NordVPN settings from environment variables using the params package.
func GetNordvpnSettings(paramsReader params.Reader) (settings models.ProviderSettings, err error) {
settings.Name = constants.Nordvpn
settings.ServerSelection.Protocol, err = paramsReader.GetNetworkProtocol()
if err != nil {
return settings, err
}
settings.ServerSelection.TargetIP, err = paramsReader.GetTargetIP()
if err != nil {
return settings, err
}
settings.ServerSelection.Region, err = paramsReader.GetNordvpnRegion()
if err != nil {
return settings, err
}
settings.ServerSelection.Number, err = paramsReader.GetNordvpnNumber()
if err != nil {
return settings, err
}
return settings, nil
}