Maintenance: split each provider in a package

- Fix VyprVPN port
- Fix missing Auth overrides
This commit is contained in:
Quentin McGaw
2021-05-11 17:10:51 +00:00
parent 1cb93d76ed
commit e8c8742bae
104 changed files with 3685 additions and 3026 deletions

View File

@@ -4,3 +4,13 @@ const (
TUN = "tun0"
TAP = "tap0"
)
const (
AES128cbc = "aes-128-cbc"
AES256cbc = "aes-256-cbc"
AES128gcm = "aes-128-gcm"
AES256gcm = "aes-256-gcm"
SHA1 = "sha1"
SHA256 = "sha256"
SHA512 = "sha512"
)

View File

@@ -2,6 +2,7 @@ package models
import (
"net"
"strconv"
)
type OpenVPNConnection struct {
@@ -15,3 +16,11 @@ func (o *OpenVPNConnection) Equal(other OpenVPNConnection) bool {
return o.IP.Equal(other.IP) && o.Port == other.Port && o.Protocol == other.Protocol &&
o.Hostname == other.Hostname
}
func (o OpenVPNConnection) RemoteLine() (line string) {
return "remote " + o.IP.String() + " " + strconv.Itoa(int(o.Port))
}
func (o OpenVPNConnection) ProtoLine() (line string) {
return "proto " + o.Protocol
}

View File

@@ -1,8 +0,0 @@
package provider
const (
aes256cbc = "aes-256-cbc"
aes128gcm = "aes-128-gcm"
aes256gcm = "aes-256-gcm"
sha256 = "sha256"
)

View File

@@ -1,150 +0,0 @@
package provider
import (
"context"
"fmt"
"math/rand"
"net"
"net/http"
"strconv"
"strings"
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
type cyberghost struct {
servers []models.CyberghostServer
randSource rand.Source
}
func newCyberghost(servers []models.CyberghostServer, timeNow timeNowFunc) *cyberghost {
return &cyberghost{
servers: servers,
randSource: rand.NewSource(timeNow().UnixNano()),
}
}
func (c *cyberghost) filterServers(regions, hostnames []string, group string) (servers []models.CyberghostServer) {
for _, server := range c.servers {
switch {
case group != "" && !strings.EqualFold(group, server.Group),
filterByPossibilities(server.Region, regions),
filterByPossibilities(server.Hostname, hostnames):
default:
servers = append(servers, server)
}
}
return servers
}
func (c *cyberghost) GetOpenVPNConnection(selection configuration.ServerSelection) (
connection models.OpenVPNConnection, err error) {
const httpsPort = 443
protocol := tcpBoolToProtocol(selection.TCP)
if selection.TargetIP != nil {
return models.OpenVPNConnection{IP: selection.TargetIP, Port: httpsPort, Protocol: protocol}, nil
}
servers := c.filterServers(selection.Regions, selection.Hostnames, selection.Group)
if len(servers) == 0 {
return connection,
fmt.Errorf("no server found for regions %s and group %q", commaJoin(selection.Regions), selection.Group)
}
var connections []models.OpenVPNConnection
for _, server := range servers {
for _, IP := range server.IPs {
connections = append(connections, models.OpenVPNConnection{IP: IP, Port: httpsPort, Protocol: protocol})
}
}
return pickRandomConnection(connections, c.randSource), nil
}
func (c *cyberghost) BuildConf(connection models.OpenVPNConnection,
username string, settings configuration.OpenVPN) (lines []string) {
if len(settings.Cipher) == 0 {
settings.Cipher = aes256cbc
}
if len(settings.Auth) == 0 {
settings.Auth = sha256
}
lines = []string{
"client",
"dev tun",
"nobind",
"persist-key",
"persist-tun",
"remote-cert-tls server",
"ping 10",
"ping-exit 60",
"ping-timer-rem",
"tls-exit",
// Cyberghost specific
// "redirect-gateway def1",
"ncp-disable",
"explicit-exit-notify 2",
"script-security 2",
"route-delay 5",
// Added constant values
"auth-nocache",
"mute-replay-warnings",
"pull-filter ignore \"auth-token\"", // prevent auth failed loops
"auth-retry nointeract",
"suppress-timestamps",
// Modified variables
fmt.Sprintf("verb %d", settings.Verbosity),
fmt.Sprintf("auth-user-pass %s", constants.OpenVPNAuthConf),
fmt.Sprintf("proto %s", connection.Protocol),
fmt.Sprintf("remote %s %d", connection.IP, connection.Port),
"data-ciphers-fallback " + settings.Cipher,
"data-ciphers " + settings.Cipher,
fmt.Sprintf("auth %s", settings.Auth),
}
if strings.HasSuffix(settings.Cipher, "-gcm") {
lines = append(lines, "ncp-ciphers AES-256-GCM:AES-256-CBC:AES-128-GCM")
}
if !settings.Root {
lines = append(lines, "user "+username)
}
if settings.MSSFix > 0 {
lines = append(lines, "mssfix "+strconv.Itoa(int(settings.MSSFix)))
}
lines = append(lines, []string{
"<ca>",
"-----BEGIN CERTIFICATE-----",
constants.CyberghostCertificate,
"-----END CERTIFICATE-----",
"</ca>",
}...)
lines = append(lines, []string{
"<cert>",
"-----BEGIN CERTIFICATE-----",
settings.Provider.ExtraConfigOptions.ClientCertificate,
"-----END CERTIFICATE-----",
"</cert>",
}...)
lines = append(lines, []string{
"<key>",
"-----BEGIN PRIVATE KEY-----",
settings.Provider.ExtraConfigOptions.ClientKey,
"-----END PRIVATE KEY-----",
"</key>",
"",
}...)
return lines
}
func (c *cyberghost) 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 cyberghost")
}

View File

@@ -0,0 +1,40 @@
package cyberghost
import (
"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 (c *Cyberghost) GetOpenVPNConnection(selection configuration.ServerSelection) (
connection models.OpenVPNConnection, err error) {
const port = 443
protocol := constants.UDP
if selection.TCP {
protocol = constants.TCP
}
servers, err := c.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,
}
connections = append(connections, connection)
}
}
if selection.TargetIP != nil {
return utils.GetTargetIPConnection(connections, selection.TargetIP)
}
return utils.PickRandomConnection(connections, c.randSource), nil
}

View File

@@ -0,0 +1,28 @@
package cyberghost
import (
"strings"
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/provider/utils"
)
func (c *Cyberghost) filterServers(selection configuration.ServerSelection) (
servers []models.CyberghostServer, err error) {
for _, server := range c.servers {
switch {
case selection.Group != "" && !strings.EqualFold(selection.Group, server.Group), // TODO make CSV
utils.FilterByPossibilities(server.Region, selection.Regions),
utils.FilterByPossibilities(server.Hostname, selection.Hostnames):
default:
servers = append(servers, server)
}
}
if len(servers) == 0 {
return nil, utils.NoServerFoundError(selection)
}
return servers, nil
}

View File

@@ -1,22 +1,26 @@
package provider
package cyberghost
import (
"errors"
"testing"
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_cyberghost_filterServers(t *testing.T) {
func Test_Cyberghost_filterServers(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
servers []models.CyberghostServer
regions []string
hostnames []string
group string
selection configuration.ServerSelection
filteredServers []models.CyberghostServer
err error
}{
"no servers": {},
"no servers": {
err: errors.New("no server found: for protocol udp"),
},
"servers without filter": {
servers: []models.CyberghostServer{
{Region: "a", Group: "1"},
@@ -38,7 +42,9 @@ func Test_cyberghost_filterServers(t *testing.T) {
{Region: "c", Group: "2"},
{Region: "d", Group: "2"},
},
regions: []string{"a", "c"},
selection: configuration.ServerSelection{
Regions: []string{"a", "c"},
},
filteredServers: []models.CyberghostServer{
{Region: "a", Group: "1"},
{Region: "c", Group: "2"},
@@ -51,7 +57,9 @@ func Test_cyberghost_filterServers(t *testing.T) {
{Region: "c", Group: "2"},
{Region: "d", Group: "2"},
},
group: "1",
selection: configuration.ServerSelection{
Group: "1",
},
filteredServers: []models.CyberghostServer{
{Region: "a", Group: "1"},
{Region: "b", Group: "1"},
@@ -64,8 +72,10 @@ func Test_cyberghost_filterServers(t *testing.T) {
{Region: "c", Group: "2"},
{Region: "d", Group: "2"},
},
regions: []string{"a", "c"},
group: "1",
selection: configuration.ServerSelection{
Regions: []string{"a", "c"},
Group: "1",
},
filteredServers: []models.CyberghostServer{
{Region: "a", Group: "1"},
},
@@ -76,7 +86,9 @@ func Test_cyberghost_filterServers(t *testing.T) {
{Hostname: "b"},
{Hostname: "c"},
},
hostnames: []string{"a", "c"},
selection: configuration.ServerSelection{
Hostnames: []string{"a", "c"},
},
filteredServers: []models.CyberghostServer{
{Hostname: "a"},
{Hostname: "c"},
@@ -87,8 +99,16 @@ func Test_cyberghost_filterServers(t *testing.T) {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
c := &cyberghost{servers: testCase.servers}
filteredServers := c.filterServers(testCase.regions, testCase.hostnames, testCase.group)
c := &Cyberghost{servers: testCase.servers}
filteredServers, err := c.filterServers(testCase.selection)
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.filteredServers, filteredServers)
})
}

View File

@@ -0,0 +1,81 @@
package cyberghost
import (
"strconv"
"strings"
"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 (c *Cyberghost) 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",
"persist-tun",
"remote-cert-tls server",
"ping 10",
"ping-exit 60",
"ping-timer-rem",
"tls-exit",
// Cyberghost specific
// "redirect-gateway def1",
"ncp-disable",
"explicit-exit-notify 2",
"script-security 2",
"route-delay 5",
// Added constant values
"auth-nocache",
"mute-replay-warnings",
"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,
connection.ProtoLine(),
connection.RemoteLine(),
"data-ciphers-fallback " + settings.Cipher,
"data-ciphers " + settings.Cipher,
"auth " + settings.Auth,
}
if strings.HasSuffix(settings.Cipher, "-gcm") {
lines = append(lines, "ncp-ciphers AES-256-GCM:AES-256-CBC:AES-128-GCM")
}
if !settings.Root {
lines = append(lines, "user "+username)
}
if settings.MSSFix > 0 {
lines = append(lines, "mssfix "+strconv.Itoa(int(settings.MSSFix)))
}
lines = append(lines, utils.WrapOpenvpnCA(
constants.CyberghostCertificate)...)
lines = append(lines, utils.WrapOpenvpnCert(
settings.Provider.ExtraConfigOptions.ClientCertificate)...)
lines = append(lines, utils.WrapOpenvpnKey(
settings.Provider.ExtraConfigOptions.ClientKey)...)
lines = append(lines, "")
return lines
}

View File

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

View File

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

View File

@@ -1,5 +0,0 @@
package provider
import "errors"
var ErrNoServerFound = errors.New("no server found")

View File

@@ -1,164 +0,0 @@
package provider
import (
"context"
"fmt"
"math/rand"
"net"
"net/http"
"strconv"
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
type fastestvpn struct {
servers []models.FastestvpnServer
randSource rand.Source
}
func newFastestvpn(servers []models.FastestvpnServer, timeNow timeNowFunc) *fastestvpn {
return &fastestvpn{
servers: servers,
randSource: rand.NewSource(timeNow().UnixNano()),
}
}
func (f *fastestvpn) filterServers(countries, hostnames []string, tcp bool) (servers []models.FastestvpnServer) {
for _, server := range f.servers {
switch {
case filterByPossibilities(server.Country, countries):
case filterByPossibilities(server.Hostname, hostnames):
case tcp && !server.TCP:
case !tcp && !server.UDP:
default:
servers = append(servers, server)
}
}
return servers
}
func (f *fastestvpn) notFoundErr(selection configuration.ServerSelection) error {
message := "no server found for protocol " + tcpBoolToProtocol(selection.TCP)
if len(selection.Hostnames) > 0 {
message += " + hostnames " + commaJoin(selection.Hostnames)
}
if len(selection.Countries) > 0 {
message += " + countries " + commaJoin(selection.Countries)
}
return fmt.Errorf(message)
}
func (f *fastestvpn) GetOpenVPNConnection(selection configuration.ServerSelection) (
connection models.OpenVPNConnection, err error) {
var port uint16 = 4443
protocol := tcpBoolToProtocol(selection.TCP)
if selection.TargetIP != nil {
return models.OpenVPNConnection{IP: selection.TargetIP, Port: port, Protocol: protocol}, nil
}
servers := f.filterServers(selection.Countries, selection.Hostnames, selection.TCP)
if len(servers) == 0 {
return connection, f.notFoundErr(selection)
}
var connections []models.OpenVPNConnection
for _, server := range servers {
for _, IP := range server.IPs {
connection := models.OpenVPNConnection{
IP: IP,
Port: port,
Protocol: protocol,
}
connections = append(connections, connection)
}
}
return pickRandomConnection(connections, f.randSource), nil
}
func (f *fastestvpn) BuildConf(connection models.OpenVPNConnection,
username string, settings configuration.OpenVPN) (lines []string) {
if len(settings.Cipher) == 0 {
settings.Cipher = aes256cbc
}
if len(settings.Auth) == 0 {
settings.Auth = sha256
}
if settings.MSSFix == 0 {
settings.MSSFix = 1450
}
lines = []string{
"client",
"dev tun",
"nobind",
"persist-key",
"ping 15",
"ping-exit 60",
"ping-timer-rem",
"tls-exit",
// Fastestvpn specific
"ping-restart 0",
"tls-client",
"tls-cipher TLS-DHE-RSA-WITH-AES-256-GCM-SHA384:TLS-DHE-RSA-WITH-AES-256-CBC-SHA256:TLS-DHE-RSA-WITH-CAMELLIA-256-CBC-SHA:TLS-DHE-RSA-WITH-AES-256-CBC-SHA:TLS-RSA-WITH-CAMELLIA-256-CBC-SHA:TLS-RSA-WITH-AES-256-CBC-SHA", //nolint:lll
"comp-lzo",
"key-direction 1",
"tun-mtu 1500",
"tun-mtu-extra 32",
"mssfix " + strconv.Itoa(int(settings.MSSFix)), // defaults to 1450
// Added constant values
"auth-nocache",
"mute-replay-warnings",
"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,
"remote " + connection.IP.String() + " " + strconv.Itoa(int(connection.Port)),
"data-ciphers-fallback " + settings.Cipher,
"data-ciphers " + settings.Cipher,
"auth " + settings.Auth,
}
if !settings.Root {
lines = append(lines, "user "+username)
}
lines = append(lines, []string{
"<ca>",
"-----BEGIN CERTIFICATE-----",
constants.FastestvpnCertificate,
"-----END CERTIFICATE-----",
"</ca>",
}...)
lines = append(lines, []string{
"<tls-auth>",
"-----BEGIN OpenVPN Static key V1-----",
constants.FastestvpnOpenvpnStaticKeyV1,
"-----END OpenVPN Static key V1-----",
"</tls-auth>",
"",
}...)
return lines
}
func (f *fastestvpn) 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 fastestvpn")
}

View File

@@ -0,0 +1,40 @@
package fastestvpn
import (
"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 (f *Fastestvpn) GetOpenVPNConnection(selection configuration.ServerSelection) (
connection models.OpenVPNConnection, err error) {
const port = 4443
protocol := constants.UDP
if selection.TCP {
protocol = constants.TCP
}
servers, err := f.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,
}
connections = append(connections, connection)
}
}
if selection.TargetIP != nil {
return utils.GetTargetIPConnection(connections, selection.TargetIP)
}
return utils.PickRandomConnection(connections, f.randSource), nil
}

View File

@@ -0,0 +1,28 @@
package fastestvpn
import (
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/provider/utils"
)
func (f *Fastestvpn) filterServers(selection configuration.ServerSelection) (
servers []models.FastestvpnServer, err error) {
for _, server := range f.servers {
switch {
case
utils.FilterByPossibilities(server.Country, selection.Countries),
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,73 @@
package fastestvpn
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 (f *Fastestvpn) 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
}
if settings.MSSFix == 0 {
settings.MSSFix = 1450
}
lines = []string{
"client",
"dev tun",
"nobind",
"persist-key",
"ping 15",
"ping-exit 60",
"ping-timer-rem",
"tls-exit",
// Fastestvpn specific
"ping-restart 0",
"tls-client",
"tls-cipher TLS-DHE-RSA-WITH-AES-256-GCM-SHA384:TLS-DHE-RSA-WITH-AES-256-CBC-SHA256:TLS-DHE-RSA-WITH-CAMELLIA-256-CBC-SHA:TLS-DHE-RSA-WITH-AES-256-CBC-SHA:TLS-RSA-WITH-CAMELLIA-256-CBC-SHA:TLS-RSA-WITH-AES-256-CBC-SHA", //nolint:lll
"comp-lzo",
"key-direction 1",
"tun-mtu 1500",
"tun-mtu-extra 32",
"mssfix " + strconv.Itoa(int(settings.MSSFix)), // defaults to 1450
// Added constant values
"auth-nocache",
"mute-replay-warnings",
"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,
connection.ProtoLine(),
connection.RemoteLine(),
"data-ciphers-fallback " + settings.Cipher,
"data-ciphers " + settings.Cipher,
"auth " + settings.Auth,
}
if !settings.Root {
lines = append(lines, "user "+username)
}
lines = append(lines, utils.WrapOpenvpnCA(
constants.FastestvpnCertificate)...)
lines = append(lines, utils.WrapOpenvpnTLSAuth(
constants.FastestvpnOpenvpnStaticKeyV1)...)
lines = append(lines, "")
return lines
}

View File

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

View File

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

View File

@@ -1,179 +0,0 @@
package provider
import (
"context"
"fmt"
"math/rand"
"net"
"net/http"
"strconv"
"strings"
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
type hideMyAss struct {
servers []models.HideMyAssServer
randSource rand.Source
}
func newHideMyAss(servers []models.HideMyAssServer, timeNow timeNowFunc) *hideMyAss {
return &hideMyAss{
servers: servers,
randSource: rand.NewSource(timeNow().UnixNano()),
}
}
func (h *hideMyAss) filterServers(countries, cities, hostnames []string,
tcp bool) (servers []models.HideMyAssServer) {
for _, server := range h.servers {
switch {
case
filterByPossibilities(server.Country, countries),
filterByPossibilities(server.City, cities),
filterByPossibilities(server.Hostname, hostnames),
tcp && !server.TCP,
!tcp && !server.UDP:
default:
servers = append(servers, server)
}
}
return servers
}
func (h *hideMyAss) notFoundErr(selection configuration.ServerSelection) error {
var filters []string
if len(selection.Countries) > 0 {
filters = append(filters, "countries "+commaJoin(selection.Countries))
}
if len(selection.Cities) > 0 {
filters = append(filters, "countries "+commaJoin(selection.Cities))
}
if len(selection.Hostnames) > 0 {
filters = append(filters, "countries "+commaJoin(selection.Hostnames))
}
return fmt.Errorf("%w for %s", ErrNoServerFound, strings.Join(filters, " + "))
}
func (h *hideMyAss) GetOpenVPNConnection(selection configuration.ServerSelection) (
connection models.OpenVPNConnection, err error) {
var defaultPort uint16 = 553
protocol := constants.UDP
if selection.TCP {
protocol = constants.TCP
defaultPort = 8080
}
port := defaultPort
if selection.CustomPort > 0 {
port = selection.CustomPort
}
if selection.TargetIP != nil {
return models.OpenVPNConnection{
IP: selection.TargetIP,
Port: port,
Protocol: protocol,
}, nil
}
servers := h.filterServers(selection.Countries, selection.Cities, selection.Hostnames, selection.TCP)
if len(servers) == 0 {
return models.OpenVPNConnection{}, h.notFoundErr(selection)
}
var connections []models.OpenVPNConnection
for _, server := range servers {
for _, IP := range server.IPs {
connections = append(connections, models.OpenVPNConnection{
IP: IP,
Port: port,
Protocol: protocol,
})
}
}
return pickRandomConnection(connections, h.randSource), nil
}
func (h *hideMyAss) BuildConf(connection models.OpenVPNConnection,
username string, settings configuration.OpenVPN) (lines []string) {
if len(settings.Cipher) == 0 {
settings.Cipher = aes256cbc
}
lines = []string{
"client",
"dev tun",
"nobind",
"persist-key",
"ping 5",
"ping-exit 30",
"ping-timer-rem",
"tls-exit",
// HideMyAss specific
"remote-cert-tls server", // updated name of ns-cert-type
// "route-metric 1",
"comp-lzo yes",
"comp-noadapt",
// 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,
"remote " + connection.IP.String() + strconv.Itoa(int(connection.Port)),
"data-ciphers-fallback " + settings.Cipher,
"data-ciphers " + settings.Cipher,
}
if !settings.Root {
lines = append(lines, "user "+username)
}
if settings.MSSFix > 0 {
lines = append(lines, "mssfix "+strconv.Itoa(int(settings.MSSFix)))
}
lines = append(lines, []string{
"<ca>",
"-----BEGIN CERTIFICATE-----",
constants.HideMyAssCA,
"-----END CERTIFICATE-----",
"</ca>",
"<cert>",
"-----BEGIN CERTIFICATE-----",
constants.HideMyAssCertificate,
"-----END CERTIFICATE-----",
"</cert>",
"<key>",
"-----BEGIN RSA PRIVATE KEY-----",
constants.HideMyAssRSAPrivateKey,
"-----END RSA PRIVATE KEY-----",
"</key>",
"",
}...)
return lines
}
func (h *hideMyAss) 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 hideMyAss")
}

View File

@@ -0,0 +1,45 @@
package hidemyass
import (
"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 (h *HideMyAss) GetOpenVPNConnection(selection configuration.ServerSelection) (
connection models.OpenVPNConnection, err error) {
var port uint16 = 553
protocol := constants.UDP
if selection.TCP {
protocol = constants.TCP
port = 8080
}
if selection.CustomPort > 0 {
port = selection.CustomPort
}
servers, err := h.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,
}
connections = append(connections, connection)
}
}
if selection.TargetIP != nil {
return utils.GetTargetIPConnection(connections, selection.TargetIP)
}
return utils.PickRandomConnection(connections, h.randSource), nil
}

View File

@@ -0,0 +1,29 @@
package hidemyass
import (
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/provider/utils"
)
func (h *HideMyAss) filterServers(selection configuration.ServerSelection) (
servers []models.HideMyAssServer, err error) {
for _, server := range h.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,72 @@
package hidemyass
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 (h *HideMyAss) BuildConf(connection models.OpenVPNConnection,
username string, settings configuration.OpenVPN) (lines []string) {
if settings.Cipher == "" {
settings.Cipher = constants.AES256cbc
}
lines = []string{
"client",
"dev tun",
"nobind",
"persist-key",
"ping 5",
"ping-exit 30",
"ping-timer-rem",
"tls-exit",
// HideMyAss specific
"remote-cert-tls server", // updated name of ns-cert-type
// "route-metric 1",
"comp-lzo yes",
"comp-noadapt",
// 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,
"remote " + connection.IP.String() + strconv.Itoa(int(connection.Port)),
"data-ciphers-fallback " + settings.Cipher,
"data-ciphers " + settings.Cipher,
}
if settings.Auth != "" {
lines = append(lines, "auth "+settings.Auth)
}
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.HideMyAssCA)...)
lines = append(lines, utils.WrapOpenvpnCert(
constants.HideMyAssCertificate)...)
lines = append(lines, utils.WrapOpenvpnRSAKey(
constants.HideMyAssRSAPrivateKey)...)
lines = append(lines, "")
return lines
}

View File

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

View File

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

View File

@@ -1,147 +0,0 @@
package provider
import (
"context"
"fmt"
"math/rand"
"net"
"net/http"
"strconv"
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
type mullvad struct {
servers []models.MullvadServer
randSource rand.Source
}
func newMullvad(servers []models.MullvadServer, timeNow timeNowFunc) *mullvad {
return &mullvad{
servers: servers,
randSource: rand.NewSource(timeNow().UnixNano()),
}
}
func (m *mullvad) filterServers(countries, cities, hostnames,
isps []string, owned bool) (servers []models.MullvadServer) {
for _, server := range m.servers {
switch {
case
filterByPossibilities(server.Country, countries),
filterByPossibilities(server.City, cities),
filterByPossibilities(server.Hostname, hostnames),
filterByPossibilities(server.ISP, isps),
owned && !server.Owned:
default:
servers = append(servers, server)
}
}
return servers
}
func (m *mullvad) GetOpenVPNConnection(selection configuration.ServerSelection) (
connection models.OpenVPNConnection, err error) {
var defaultPort uint16 = 1194
protocol := constants.UDP
if selection.TCP {
defaultPort = 443
protocol = constants.TCP
}
port := defaultPort
if selection.CustomPort > 0 {
port = selection.CustomPort
}
if selection.TargetIP != nil {
return models.OpenVPNConnection{IP: selection.TargetIP, Port: port, Protocol: protocol}, nil
}
servers := m.filterServers(selection.Countries, selection.Cities,
selection.Hostnames, selection.ISPs, selection.Owned)
if len(servers) == 0 {
return connection, fmt.Errorf("no server found for countries %s, cities %s, ISPs %s and owned %t",
commaJoin(selection.Countries), commaJoin(selection.Cities), commaJoin(selection.ISPs), selection.Owned)
}
var connections []models.OpenVPNConnection
for _, server := range servers {
for _, IP := range server.IPs {
connections = append(connections, models.OpenVPNConnection{IP: IP, Port: port, Protocol: protocol})
}
}
return pickRandomConnection(connections, m.randSource), nil
}
func (m *mullvad) BuildConf(connection models.OpenVPNConnection,
username string, settings configuration.OpenVPN) (lines []string) {
if len(settings.Cipher) == 0 {
settings.Cipher = aes256cbc
}
lines = []string{
"client",
"dev tun",
"nobind",
"persist-key",
"remote-cert-tls server",
"ping 10",
"ping-exit 60",
"ping-timer-rem",
"tls-exit",
// Mullvad specific
"sndbuf 524288",
"rcvbuf 524288",
"tls-cipher TLS-DHE-RSA-WITH-AES-256-GCM-SHA384:TLS-DHE-RSA-WITH-AES-256-CBC-SHA",
"fast-io",
"script-security 2",
// Added constant values
"mute-replay-warnings",
"auth-nocache",
"pull-filter ignore \"auth-token\"", // prevent auth failed loops
"auth-retry nointeract",
"suppress-timestamps",
// Modified variables
fmt.Sprintf("verb %d", settings.Verbosity),
fmt.Sprintf("auth-user-pass %s", constants.OpenVPNAuthConf),
fmt.Sprintf("proto %s", connection.Protocol),
fmt.Sprintf("remote %s %d", connection.IP, connection.Port),
"data-ciphers-fallback " + settings.Cipher,
"data-ciphers " + settings.Cipher,
}
if settings.Provider.ExtraConfigOptions.OpenVPNIPv6 {
lines = append(lines, "tun-ipv6")
} else {
lines = append(lines, `pull-filter ignore "route-ipv6"`)
lines = append(lines, `pull-filter ignore "ifconfig-ipv6"`)
}
if !settings.Root {
lines = append(lines, "user "+username)
}
if settings.MSSFix > 0 {
lines = append(lines, "mssfix "+strconv.Itoa(int(settings.MSSFix)))
}
lines = append(lines, []string{
"<ca>",
"-----BEGIN CERTIFICATE-----",
constants.MullvadCertificate,
"-----END CERTIFICATE-----",
"</ca>",
"",
}...)
return lines
}
func (m *mullvad) 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 mullvad")
}

View File

@@ -0,0 +1,45 @@
package mullvad
import (
"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 (m *Mullvad) GetOpenVPNConnection(selection configuration.ServerSelection) (
connection models.OpenVPNConnection, err error) {
var port uint16 = 1194
protocol := constants.UDP
if selection.TCP {
port = 443
protocol = constants.TCP
}
if selection.CustomPort > 0 {
port = selection.CustomPort
}
servers, err := m.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,
}
connections = append(connections, connection)
}
}
if selection.TargetIP != nil {
return utils.GetTargetIPConnection(connections, selection.TargetIP)
}
return utils.PickRandomConnection(connections, m.randSource), nil
}

View File

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

View File

@@ -0,0 +1,77 @@
package mullvad
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 (m *Mullvad) BuildConf(connection models.OpenVPNConnection,
username string, settings configuration.OpenVPN) (lines []string) {
if settings.Cipher == "" {
settings.Cipher = constants.AES256cbc
}
lines = []string{
"client",
"dev tun",
"nobind",
"persist-key",
"remote-cert-tls server",
"ping 10",
"ping-exit 60",
"ping-timer-rem",
"tls-exit",
// Mullvad specific
"sndbuf 524288",
"rcvbuf 524288",
"tls-cipher TLS-DHE-RSA-WITH-AES-256-GCM-SHA384:TLS-DHE-RSA-WITH-AES-256-CBC-SHA",
"fast-io",
"script-security 2",
// Added constant values
"auth-nocache",
"mute-replay-warnings",
"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,
connection.ProtoLine(),
connection.RemoteLine(),
"data-ciphers-fallback " + settings.Cipher,
"data-ciphers " + settings.Cipher,
}
if settings.Auth != "" {
lines = append(lines, "auth "+settings.Auth)
}
if settings.Provider.ExtraConfigOptions.OpenVPNIPv6 {
lines = append(lines, "tun-ipv6")
} else {
lines = append(lines, `pull-filter ignore "route-ipv6"`)
lines = append(lines, `pull-filter ignore "ifconfig-ipv6"`)
}
if !settings.Root {
lines = append(lines, "user "+username)
}
if settings.MSSFix > 0 {
lines = append(lines, "mssfix "+strconv.Itoa(int(settings.MSSFix)))
}
lines = append(lines, utils.WrapOpenvpnCA(
constants.MullvadCertificate)...)
lines = append(lines, "")
return lines
}

View File

@@ -0,0 +1,17 @@
package mullvad
import (
"context"
"net"
"net/http"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
func (m *Mullvad) 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 logic is not needed for Mullvad")
}

View File

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

View File

@@ -1,184 +0,0 @@
package provider
import (
"context"
"errors"
"fmt"
"math/rand"
"net"
"net/http"
"strconv"
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
type nordvpn struct {
servers []models.NordvpnServer
randSource rand.Source
}
func newNordvpn(servers []models.NordvpnServer, timeNow timeNowFunc) *nordvpn {
return &nordvpn{
servers: servers,
randSource: rand.NewSource(timeNow().UnixNano()),
}
}
func (n *nordvpn) filterServers(regions, hostnames, names []string, numbers []uint16, tcp bool) (
servers []models.NordvpnServer) {
numbersStr := make([]string, len(numbers))
for i := range numbers {
numbersStr[i] = fmt.Sprintf("%d", numbers[i])
}
for _, server := range n.servers {
numberStr := fmt.Sprintf("%d", server.Number)
switch {
case
tcp && !server.TCP,
!tcp && !server.UDP,
filterByPossibilities(server.Region, regions),
filterByPossibilities(server.Hostname, hostnames),
filterByPossibilities(server.Name, names),
filterByPossibilities(numberStr, numbersStr):
default:
servers = append(servers, server)
}
}
return servers
}
var errNoServerFound = errors.New("no server found")
func (n *nordvpn) notFoundErr(selection configuration.ServerSelection) error {
message := "for protocol " + tcpBoolToProtocol(selection.TCP)
if len(selection.Regions) > 0 {
message += " + regions " + commaJoin(selection.Regions)
}
if len(selection.Hostnames) > 0 {
message += " + hostnames " + commaJoin(selection.Hostnames)
}
if len(selection.Names) > 0 {
message += " + names " + commaJoin(selection.Names)
}
if len(selection.Numbers) > 0 {
numbers := make([]string, len(selection.Numbers))
for i, n := range selection.Numbers {
numbers[i] = strconv.Itoa(int(n))
}
message += " + numbers " + commaJoin(numbers)
}
return fmt.Errorf("%w: %s", errNoServerFound, message)
}
func (n *nordvpn) GetOpenVPNConnection(selection configuration.ServerSelection) (
connection models.OpenVPNConnection, err error) {
var port uint16 = 1194
protocol := constants.UDP
if selection.TCP {
port = 443
protocol = constants.TCP
}
if selection.TargetIP != nil {
return models.OpenVPNConnection{IP: selection.TargetIP, Port: port, Protocol: protocol}, nil
}
servers := n.filterServers(selection.Regions, selection.Hostnames,
selection.Names, selection.Numbers, selection.TCP)
if len(servers) == 0 {
return connection, n.notFoundErr(selection)
}
connections := make([]models.OpenVPNConnection, len(servers))
for i := range servers {
connections[i] = models.OpenVPNConnection{IP: servers[i].IP, Port: port, Protocol: protocol}
}
return pickRandomConnection(connections, n.randSource), nil
}
func (n *nordvpn) BuildConf(connection models.OpenVPNConnection,
username string, settings configuration.OpenVPN) (lines []string) {
if len(settings.Cipher) == 0 {
settings.Cipher = aes256cbc
}
if len(settings.Auth) == 0 {
settings.Auth = "sha512"
}
const defaultMSSFix = 1450
if settings.MSSFix == 0 {
settings.MSSFix = defaultMSSFix
}
lines = []string{
"client",
"dev tun",
"nobind",
"persist-key",
"remote-cert-tls server",
"ping-timer-rem",
"tls-exit",
// Nordvpn specific
"tun-mtu 1500",
"tun-mtu-extra 32",
"mssfix " + strconv.Itoa(int(settings.MSSFix)),
"reneg-sec 0",
"comp-lzo no",
"fast-io",
"key-direction 1",
"ping 15",
"ping-restart 0",
// Added constant values
"auth-nocache",
"mute-replay-warnings",
"pull-filter ignore \"auth-token\"", // prevent auth failed loops
"auth-retry nointeract",
"suppress-timestamps",
// Modified variables
fmt.Sprintf("verb %d", settings.Verbosity),
fmt.Sprintf("auth-user-pass %s", constants.OpenVPNAuthConf),
fmt.Sprintf("proto %s", connection.Protocol),
fmt.Sprintf("remote %s %d", connection.IP.String(), connection.Port),
"data-ciphers-fallback " + settings.Cipher,
"data-ciphers " + settings.Cipher,
fmt.Sprintf("auth %s", settings.Auth),
}
if !settings.Root {
lines = append(lines, "user "+username)
}
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) 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 nordvpn")
}

View File

@@ -0,0 +1,39 @@
package nordvpn
import (
"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 (n *Nordvpn) GetOpenVPNConnection(selection configuration.ServerSelection) (
connection models.OpenVPNConnection, err error) {
var port uint16 = 1194
protocol := constants.UDP
if selection.TCP {
port = 443
protocol = constants.TCP
}
servers, err := n.filterServers(selection)
if err != nil {
return connection, err
}
connections := make([]models.OpenVPNConnection, len(servers))
for i := range servers {
connection := models.OpenVPNConnection{
IP: servers[i].IP,
Port: port,
Protocol: protocol,
}
connections[i] = connection
}
if selection.TargetIP != nil {
return utils.GetTargetIPConnection(connections, selection.TargetIP)
}
return utils.PickRandomConnection(connections, n.randSource), nil
}

View File

@@ -0,0 +1,38 @@
package nordvpn
import (
"strconv"
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/provider/utils"
)
func (n *Nordvpn) filterServers(selection configuration.ServerSelection) (
servers []models.NordvpnServer, err error) {
selectedNumbers := make([]string, len(selection.Numbers))
for i := range selection.Numbers {
selectedNumbers[i] = strconv.Itoa(int(selection.Numbers[i]))
}
for _, server := range n.servers {
serverNumber := strconv.Itoa(int(server.Number))
switch {
case
utils.FilterByPossibilities(server.Region, selection.Regions),
utils.FilterByPossibilities(server.Hostname, selection.Hostnames),
utils.FilterByPossibilities(server.Name, selection.Names),
utils.FilterByPossibilities(serverNumber, selectedNumbers),
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,75 @@
package nordvpn
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 (n *Nordvpn) BuildConf(connection models.OpenVPNConnection,
username string, settings configuration.OpenVPN) (lines []string) {
if settings.Cipher == "" {
settings.Cipher = constants.AES256cbc
}
if settings.Auth == "" {
settings.Auth = constants.SHA512
}
if settings.MSSFix == 0 {
settings.MSSFix = 1450
}
lines = []string{
"client",
"dev tun",
"nobind",
"persist-key",
"remote-cert-tls server",
"ping-timer-rem",
"tls-exit",
// Nordvpn specific
"tun-mtu 1500",
"tun-mtu-extra 32",
"mssfix " + strconv.Itoa(int(settings.MSSFix)),
"reneg-sec 0",
"comp-lzo no",
"fast-io",
"key-direction 1",
"ping 15",
"ping-restart 0",
// Added constant values
"auth-nocache",
"mute-replay-warnings",
"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,
connection.ProtoLine(),
connection.RemoteLine(),
"data-ciphers-fallback " + settings.Cipher,
"data-ciphers " + settings.Cipher,
"auth " + settings.Auth,
}
if !settings.Root {
lines = append(lines, "user "+username)
}
lines = append(lines, utils.WrapOpenvpnCA(
constants.NordvpnCertificate)...)
lines = append(lines, utils.WrapOpenvpnTLSAuth(
constants.NordvpnOpenvpnStaticKeyV1)...)
lines = append(lines, "")
return lines
}

View File

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

View File

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

View File

@@ -1,680 +0,0 @@
package provider
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"math/rand"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/firewall"
gluetunLog "github.com/qdm12/gluetun/internal/logging"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
type pia struct {
servers []models.PIAServer
timeNow timeNowFunc
randSource rand.Source
activeServer models.PIAServer
}
func newPrivateInternetAccess(servers []models.PIAServer, timeNow timeNowFunc) *pia {
return &pia{
servers: servers,
timeNow: timeNow,
randSource: rand.NewSource(timeNow().UnixNano()),
}
}
var (
ErrInvalidPort = errors.New("invalid port number")
)
func (p *pia) getPort(selection configuration.ServerSelection) (port uint16, err error) {
if selection.CustomPort == 0 {
if selection.TCP {
switch selection.EncryptionPreset {
case constants.PIAEncryptionPresetNormal:
port = 502
case constants.PIAEncryptionPresetStrong:
port = 501
}
} else {
switch selection.EncryptionPreset {
case constants.PIAEncryptionPresetNormal:
port = 1198
case constants.PIAEncryptionPresetStrong:
port = 1197
}
}
return port, nil
}
port = selection.CustomPort
if selection.TCP {
switch port {
case 80, 110, 443: //nolint:gomnd
return port, nil
default:
return 0, fmt.Errorf("%w: %d for protocol TCP", ErrInvalidPort, port)
}
}
switch port {
case 53, 1194, 1197, 1198, 8080, 9201: //nolint:gomnd
return port, nil
default:
return 0, fmt.Errorf("%w: %d for protocol UDP", ErrInvalidPort, port)
}
}
func (p *pia) notFoundErr(regions, hostnames, names []string, tcp bool) error {
message := "for protocol " + tcpBoolToProtocol(tcp)
if len(regions) > 0 {
message += " + regions " + commaJoin(regions)
}
if len(hostnames) > 0 {
message += " + hostnames " + commaJoin(hostnames)
}
if len(names) > 0 {
message += " + names " + commaJoin(names)
}
return fmt.Errorf("%w: %s", errNoServerFound, message)
}
func (p *pia) GetOpenVPNConnection(selection configuration.ServerSelection) (
connection models.OpenVPNConnection, err error) {
port, err := p.getPort(selection)
if err != nil {
return connection, err
}
protocol := tcpBoolToProtocol(selection.TCP)
servers := p.servers
if selection.TargetIP != nil {
connection = models.OpenVPNConnection{IP: selection.TargetIP, Port: port, Protocol: protocol}
} else {
servers := p.filterServers(selection.Regions, selection.Hostnames,
selection.Names, selection.TCP)
if len(servers) == 0 {
return connection, p.notFoundErr(selection.Regions, selection.Hostnames,
selection.Names, selection.TCP)
}
var connections []models.OpenVPNConnection
for _, server := range servers {
for _, ip := range server.IPs {
connection := models.OpenVPNConnection{
IP: ip,
Port: port,
Protocol: protocol,
}
connections = append(connections, connection)
}
}
connection = pickRandomConnection(connections, p.randSource)
}
// Reverse lookup server from picked connection
found := false
for _, server := range servers {
for _, ip := range server.IPs {
if connection.IP.Equal(ip) {
p.activeServer = server
found = true
break
}
}
if found {
break
}
}
return connection, nil
}
func (p *pia) BuildConf(connection models.OpenVPNConnection,
username string, settings configuration.OpenVPN) (lines []string) {
var X509CRL, certificate string
var defaultCipher, defaultAuth string
if settings.Provider.ExtraConfigOptions.EncryptionPreset == constants.PIAEncryptionPresetNormal {
defaultCipher = "aes-128-cbc"
defaultAuth = "sha1"
X509CRL = constants.PiaX509CRLNormal
certificate = constants.PIACertificateNormal
} else { // strong encryption
defaultCipher = aes256cbc
defaultAuth = "sha256"
X509CRL = constants.PiaX509CRLStrong
certificate = constants.PIACertificateStrong
}
if len(settings.Cipher) == 0 {
settings.Cipher = defaultCipher
}
if len(settings.Auth) == 0 {
settings.Auth = defaultAuth
}
lines = []string{
"client",
"dev tun",
"nobind",
"persist-key",
"remote-cert-tls server",
// PIA specific
"reneg-sec 0",
"disable-occ",
"compress", // allow PIA server to choose the compression to use
// Added constant values
"auth-nocache",
"mute-replay-warnings",
"pull-filter ignore \"auth-token\"", // prevent auth failed loops
"auth-retry nointeract",
"suppress-timestamps",
// Modified variables
fmt.Sprintf("verb %d", settings.Verbosity),
fmt.Sprintf("auth-user-pass %s", constants.OpenVPNAuthConf),
fmt.Sprintf("proto %s", connection.Protocol),
fmt.Sprintf("remote %s %d", connection.IP, connection.Port),
"data-ciphers-fallback " + settings.Cipher,
"data-ciphers " + settings.Cipher,
fmt.Sprintf("auth %s", settings.Auth),
}
if strings.HasSuffix(settings.Cipher, "-gcm") {
lines = append(lines, "ncp-disable")
}
if !settings.Root {
lines = append(lines, "user "+username)
}
if settings.MSSFix > 0 {
lines = append(lines, "mssfix "+strconv.Itoa(int(settings.MSSFix)))
}
lines = append(lines, []string{
"<crl-verify>",
"-----BEGIN X509 CRL-----",
X509CRL,
"-----END X509 CRL-----",
"</crl-verify>",
}...)
lines = append(lines, []string{
"<ca>",
"-----BEGIN CERTIFICATE-----",
certificate,
"-----END CERTIFICATE-----",
"</ca>",
"",
}...)
return lines
}
//nolint:gocognit
func (p *pia) 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)) {
commonName := p.activeServer.ServerName
if !p.activeServer.PortForward {
pfLogger.Error("The server %s (region %s) does not support port forwarding",
commonName, p.activeServer.Region)
return
}
if gateway == nil {
pfLogger.Error("aborting because: VPN gateway IP address was not found")
return
}
privateIPClient, err := newPIAHTTPClient(commonName)
if err != nil {
pfLogger.Error("aborting because: %s", err)
return
}
defer pfLogger.Warn("loop exited")
data, err := readPIAPortForwardData(openFile)
if err != nil {
pfLogger.Error(err)
}
dataFound := data.Port > 0
durationToExpiration := data.Expiration.Sub(p.timeNow())
expired := durationToExpiration <= 0
if dataFound {
pfLogger.Info("Found persistent forwarded port data for port %d", data.Port)
if expired {
pfLogger.Warn("Forwarded port data expired on %s, getting another one", data.Expiration.Format(time.RFC1123))
} else {
pfLogger.Info("Forwarded port data expires in %s", gluetunLog.FormatDuration(durationToExpiration))
}
}
if !dataFound || expired {
tryUntilSuccessful(ctx, pfLogger, func() error {
data, err = refreshPIAPortForwardData(ctx, client, privateIPClient, gateway, openFile)
return err
})
if ctx.Err() != nil {
return
}
durationToExpiration = data.Expiration.Sub(p.timeNow())
}
pfLogger.Info("Port forwarded is %d expiring in %s", data.Port, gluetunLog.FormatDuration(durationToExpiration))
// First time binding
tryUntilSuccessful(ctx, pfLogger, func() error {
if err := bindPIAPort(ctx, privateIPClient, gateway, data); err != nil {
return fmt.Errorf("cannot bind port: %w", err)
}
return nil
})
if ctx.Err() != nil {
return
}
filepath := syncState(data.Port)
pfLogger.Info("Writing port to %s", filepath)
if err := writePortForwardedToFile(openFile, filepath, data.Port); err != nil {
pfLogger.Error(err)
}
if err := fw.SetAllowedPort(ctx, data.Port, string(constants.TUN)); err != nil {
pfLogger.Error(err)
}
expiryTimer := time.NewTimer(durationToExpiration)
const keepAlivePeriod = 15 * time.Minute
// Timer behaving as a ticker
keepAliveTimer := time.NewTimer(keepAlivePeriod)
for {
select {
case <-ctx.Done():
removeCtx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if err := fw.RemoveAllowedPort(removeCtx, data.Port); err != nil {
pfLogger.Error(err)
}
if !keepAliveTimer.Stop() {
<-keepAliveTimer.C
}
if !expiryTimer.Stop() {
<-expiryTimer.C
}
return
case <-keepAliveTimer.C:
if err := bindPIAPort(ctx, privateIPClient, gateway, data); err != nil {
pfLogger.Error("cannot bind port: " + err.Error())
}
keepAliveTimer.Reset(keepAlivePeriod)
case <-expiryTimer.C:
pfLogger.Warn("Forward port has expired on %s, getting another one", data.Expiration.Format(time.RFC1123))
oldPort := data.Port
for {
data, err = refreshPIAPortForwardData(ctx, client, privateIPClient, gateway, openFile)
if err != nil {
pfLogger.Error(err)
continue
}
break
}
durationToExpiration := data.Expiration.Sub(p.timeNow())
pfLogger.Info("Port forwarded is %d expiring in %s", data.Port, gluetunLog.FormatDuration(durationToExpiration))
if err := fw.RemoveAllowedPort(ctx, oldPort); err != nil {
pfLogger.Error(err)
}
if err := fw.SetAllowedPort(ctx, data.Port, string(constants.TUN)); err != nil {
pfLogger.Error(err)
}
filepath := syncState(data.Port)
pfLogger.Info("Writing port to %s", filepath)
if err := writePortForwardedToFile(openFile, filepath, data.Port); err != nil {
pfLogger.Error(err)
}
if err := bindPIAPort(ctx, privateIPClient, gateway, data); err != nil {
pfLogger.Error("cannot bind port: " + err.Error())
}
if !keepAliveTimer.Stop() {
<-keepAliveTimer.C
}
keepAliveTimer.Reset(keepAlivePeriod)
expiryTimer.Reset(durationToExpiration)
}
}
}
func (p *pia) filterServers(regions, hostnames, names []string, tcp bool) (
filtered []models.PIAServer) {
for _, server := range p.servers {
switch {
case filterByPossibilities(server.Region, regions),
filterByPossibilities(server.Hostname, hostnames),
filterByPossibilities(server.ServerName, names),
tcp && !server.TCP,
!tcp && !server.UDP:
default:
filtered = append(filtered, server)
}
}
return filtered
}
func newPIAHTTPClient(serverName string) (client *http.Client, err error) {
certificateBytes, err := base64.StdEncoding.DecodeString(constants.PIACertificateStrong)
if err != nil {
return nil, fmt.Errorf("cannot decode PIA root certificate: %w", err)
}
certificate, err := x509.ParseCertificate(certificateBytes)
if err != nil {
return nil, fmt.Errorf("cannot parse PIA root certificate: %w", err)
}
//nolint:gomnd
transport := &http.Transport{
// Settings taken from http.DefaultTransport
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
rootCAs := x509.NewCertPool()
rootCAs.AddCert(certificate)
transport.TLSClientConfig = &tls.Config{
RootCAs: rootCAs,
MinVersion: tls.VersionTLS12,
ServerName: serverName,
}
const httpTimeout = 30 * time.Second
return &http.Client{
Transport: transport,
Timeout: httpTimeout,
}, nil
}
func refreshPIAPortForwardData(ctx context.Context, client, privateIPClient *http.Client,
gateway net.IP, openFile os.OpenFileFunc) (data piaPortForwardData, err error) {
data.Token, err = fetchPIAToken(ctx, openFile, client)
if err != nil {
return data, fmt.Errorf("cannot obtain token: %w", err)
}
data.Port, data.Signature, data.Expiration, err = fetchPIAPortForwardData(ctx, privateIPClient, gateway, data.Token)
if err != nil {
return data, fmt.Errorf("cannot obtain port forwarding data: %w", err)
}
if err := writePIAPortForwardData(openFile, data); err != nil {
return data, fmt.Errorf("cannot persist port forwarding information to file: %w", err)
}
return data, nil
}
type piaPayload struct {
Token string `json:"token"`
Port uint16 `json:"port"`
Expiration time.Time `json:"expires_at"`
}
type piaPortForwardData struct {
Port uint16 `json:"port"`
Token string `json:"token"`
Signature string `json:"signature"`
Expiration time.Time `json:"expires_at"`
}
func readPIAPortForwardData(openFile os.OpenFileFunc) (data piaPortForwardData, err error) {
file, err := openFile(constants.PIAPortForward, os.O_RDONLY, 0)
if os.IsNotExist(err) {
return data, nil
} else if err != nil {
return data, err
}
decoder := json.NewDecoder(file)
err = decoder.Decode(&data)
if err != nil {
_ = file.Close()
return data, err
}
return data, file.Close()
}
func writePIAPortForwardData(openFile os.OpenFileFunc, data piaPortForwardData) (err error) {
file, err := openFile(constants.PIAPortForward,
os.O_CREATE|os.O_TRUNC|os.O_WRONLY,
0644)
if err != nil {
return err
}
encoder := json.NewEncoder(file)
err = encoder.Encode(data)
if err != nil {
_ = file.Close()
return err
}
return file.Close()
}
func unpackPIAPayload(payload string) (port uint16, token string, expiration time.Time, err error) {
b, err := base64.StdEncoding.DecodeString(payload)
if err != nil {
return 0, "", expiration,
fmt.Errorf("cannot decode payload: payload is %q: %w", payload, err)
}
var payloadData piaPayload
if err := json.Unmarshal(b, &payloadData); err != nil {
return 0, "", expiration,
fmt.Errorf("cannot parse payload data: data is %q: %w", string(b), err)
}
return payloadData.Port, payloadData.Token, payloadData.Expiration, nil
}
func packPIAPayload(port uint16, token string, expiration time.Time) (payload string, err error) {
payloadData := piaPayload{
Token: token,
Port: port,
Expiration: expiration,
}
b, err := json.Marshal(&payloadData)
if err != nil {
return "", fmt.Errorf("cannot serialize payload data: %w", err)
}
payload = base64.StdEncoding.EncodeToString(b)
return payload, nil
}
func fetchPIAToken(ctx context.Context, openFile os.OpenFileFunc,
client *http.Client) (token string, err error) {
username, password, err := getOpenvpnCredentials(openFile)
if err != nil {
return "", fmt.Errorf("cannot get Openvpn credentials: %w", err)
}
url := url.URL{
Scheme: "https",
User: url.UserPassword(username, password),
Host: "privateinternetaccess.com",
Path: "/gtoken/generateToken",
}
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil)
if err != nil {
return "", replaceInErr(err, map[string]string{
username: "<username>", password: "<password>"})
}
response, err := client.Do(request)
if err != nil {
return "", replaceInErr(err, map[string]string{
username: "<username>", password: "<password>"})
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
b, _ := ioutil.ReadAll(response.Body)
shortenMessage := string(b)
shortenMessage = strings.ReplaceAll(shortenMessage, "\n", "")
shortenMessage = strings.ReplaceAll(shortenMessage, " ", " ")
return "", fmt.Errorf("%s: response received: %q", response.Status, shortenMessage)
}
decoder := json.NewDecoder(response.Body)
var result struct {
Token string `json:"token"`
}
if err := decoder.Decode(&result); err != nil {
return "", err
} else if len(result.Token) == 0 {
return "", fmt.Errorf("token is empty")
}
return result.Token, nil
}
func getOpenvpnCredentials(openFile os.OpenFileFunc) (username, password string, err error) {
file, err := openFile(constants.OpenVPNAuthConf, os.O_RDONLY, 0)
if err != nil {
return "", "", fmt.Errorf("cannot read openvpn auth file: %s", err)
}
authData, err := ioutil.ReadAll(file)
if err != nil {
_ = file.Close()
return "", "", fmt.Errorf("cannot read openvpn auth file: %s", err)
}
if err := file.Close(); err != nil {
return "", "", err
}
lines := strings.Split(string(authData), "\n")
const minLines = 2
if len(lines) < minLines {
return "", "", fmt.Errorf("not enough lines (%d) in openvpn auth file", len(lines))
}
username, password = lines[0], lines[1]
return username, password, nil
}
func fetchPIAPortForwardData(ctx context.Context, client *http.Client, gateway net.IP, token string) (
port uint16, signature string, expiration time.Time, err error) {
queryParams := url.Values{}
queryParams.Add("token", token)
url := url.URL{
Scheme: "https",
Host: net.JoinHostPort(gateway.String(), "19999"),
Path: "/getSignature",
RawQuery: queryParams.Encode(),
}
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil)
if err != nil {
err = replaceInErr(err, map[string]string{token: "<token>"})
return 0, "", expiration, fmt.Errorf("cannot obtain signature: %w", err)
}
response, err := client.Do(request)
if err != nil {
err = replaceInErr(err, map[string]string{token: "<token>"})
return 0, "", expiration, fmt.Errorf("cannot obtain signature: %w", err)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return 0, "", expiration, fmt.Errorf("cannot obtain signature: %s", response.Status)
}
decoder := json.NewDecoder(response.Body)
var data struct {
Status string `json:"status"`
Payload string `json:"payload"`
Signature string `json:"signature"`
}
if err := decoder.Decode(&data); err != nil {
return 0, "", expiration, fmt.Errorf("cannot decode received data: %w", err)
} else if data.Status != "OK" {
return 0, "", expiration, fmt.Errorf("response received from PIA has status %s", data.Status)
}
port, _, expiration, err = unpackPIAPayload(data.Payload)
return port, data.Signature, expiration, err
}
func bindPIAPort(ctx context.Context, client *http.Client, gateway net.IP, data piaPortForwardData) (err error) {
payload, err := packPIAPayload(data.Port, data.Token, data.Expiration)
if err != nil {
return err
}
queryParams := url.Values{}
queryParams.Add("payload", payload)
queryParams.Add("signature", data.Signature)
url := url.URL{
Scheme: "https",
Host: net.JoinHostPort(gateway.String(), "19999"),
Path: "/bindPort",
RawQuery: queryParams.Encode(),
}
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil)
if err != nil {
return replaceInErr(err, map[string]string{
payload: "<payload>",
data.Signature: "<signature>",
})
}
response, err := client.Do(request)
if err != nil {
return replaceInErr(err, map[string]string{
payload: "<payload>",
data.Signature: "<signature>",
})
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return fmt.Errorf("cannot bind port: %s", response.Status)
}
decoder := json.NewDecoder(response.Body)
var responseData struct {
Status string `json:"status"`
Message string `json:"message"`
}
if err := decoder.Decode(&responseData); err != nil {
return err
} else if responseData.Status != "OK" {
return fmt.Errorf("response received from PIA: %s (%s)", responseData.Status, responseData.Message)
}
return nil
}
func writePortForwardedToFile(openFile os.OpenFileFunc,
filepath string, port uint16) (err error) {
file, err := openFile(filepath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
if err != nil {
return err
}
_, err = file.Write([]byte(fmt.Sprintf("%d", port)))
if err != nil {
_ = file.Close()
return err
}
return file.Close()
}
// replaceInErr is used to remove sensitive information from logs.
func replaceInErr(err error, substitutions map[string]string) error {
s := err.Error()
for old, new := range substitutions {
s = strings.ReplaceAll(s, old, new)
}
return errors.New(s)
}

View File

@@ -1,168 +0,0 @@
package provider
import (
"context"
"errors"
"fmt"
"math/rand"
"net"
"net/http"
"strconv"
"strings"
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
type privado struct {
servers []models.PrivadoServer
randSource rand.Source
}
func newPrivado(servers []models.PrivadoServer, timeNow timeNowFunc) *privado {
return &privado{
servers: servers,
randSource: rand.NewSource(timeNow().UnixNano()),
}
}
func (p *privado) filterServers(countries, regions, cities, hostnames []string) (servers []models.PrivadoServer) {
for _, server := range p.servers {
switch {
case filterByPossibilities(server.Country, countries),
filterByPossibilities(server.Region, regions),
filterByPossibilities(server.City, cities),
filterByPossibilities(server.Hostname, hostnames):
default:
servers = append(servers, server)
}
}
return servers
}
func (p *privado) notFoundErr(countries, regions, cities, hostnames []string) error {
var message string
if len(countries) > 0 {
message += " + countries " + commaJoin(countries)
}
if len(regions) > 0 {
message += " + regions " + commaJoin(regions)
}
if len(cities) > 0 {
message += " + cities " + commaJoin(cities)
}
if len(hostnames) > 0 {
message += " + hostnames " + commaJoin(hostnames)
}
message = "for " + strings.TrimPrefix(message, " +")
return fmt.Errorf("%w: %s", errNoServerFound, message)
}
var ErrProtocolUnsupported = errors.New("network protocol is not supported")
func (p *privado) GetOpenVPNConnection(selection configuration.ServerSelection) (
connection models.OpenVPNConnection, err error) {
var port uint16 = 1194
const protocol = constants.UDP
if selection.TCP {
return connection, fmt.Errorf("%w: TCP for provider Privado", ErrProtocolUnsupported)
}
if selection.TargetIP != nil {
return models.OpenVPNConnection{
IP: selection.TargetIP,
Port: port,
Protocol: protocol,
}, nil
}
servers := p.filterServers(selection.Countries, selection.Regions,
selection.Cities, selection.Hostnames)
if len(servers) == 0 {
return connection, p.notFoundErr(selection.Countries,
selection.Regions, selection.Cities, selection.Hostnames)
}
connections := make([]models.OpenVPNConnection, len(servers))
for i := range servers {
connection := models.OpenVPNConnection{
IP: servers[i].IP,
Port: port,
Protocol: protocol,
Hostname: servers[i].Hostname,
}
connections[i] = connection
}
return pickRandomConnection(connections, p.randSource), nil
}
func (p *privado) BuildConf(connection models.OpenVPNConnection,
username string, settings configuration.OpenVPN) (lines []string) {
if len(settings.Cipher) == 0 {
settings.Cipher = aes256cbc
}
if len(settings.Auth) == 0 {
settings.Auth = sha256
}
lines = []string{
"client",
"dev tun",
"nobind",
"persist-key",
"ping 10",
"ping-exit 60",
"ping-timer-rem",
"tls-exit",
// Privado specific
"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",
fmt.Sprintf("verify-x509-name %s name", connection.Hostname),
// Added constant values
"auth-nocache",
"mute-replay-warnings",
"pull-filter ignore \"auth-token\"", // prevent auth failed loops
"auth-retry nointeract",
"suppress-timestamps",
// Modified variables
fmt.Sprintf("verb %d", settings.Verbosity),
fmt.Sprintf("auth-user-pass %s", constants.OpenVPNAuthConf),
fmt.Sprintf("proto %s", connection.Protocol),
fmt.Sprintf("remote %s %d", connection.IP, connection.Port),
"data-ciphers-fallback " + settings.Cipher,
"data-ciphers " + settings.Cipher,
fmt.Sprintf("auth %s", settings.Auth),
}
if !settings.Root {
lines = append(lines, "user "+username)
}
if settings.MSSFix > 0 {
lines = append(lines, "mssfix "+strconv.Itoa(int(settings.MSSFix)))
}
lines = append(lines, []string{
"<ca>",
"-----BEGIN CERTIFICATE-----",
constants.PrivadoCertificate,
"-----END CERTIFICATE-----",
"</ca>",
}...)
return lines
}
func (p *privado) 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 privado")
}

View File

@@ -0,0 +1,44 @@
package privado
import (
"errors"
"fmt"
"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 (p *Privado) GetOpenVPNConnection(selection configuration.ServerSelection) (
connection models.OpenVPNConnection, err error) {
const port = 1194
const protocol = constants.UDP
if selection.TCP {
return connection, fmt.Errorf("%w: TCP for provider Privado", ErrProtocolUnsupported)
}
servers, err := p.filterServers(selection)
if err != nil {
return connection, err
}
connections := make([]models.OpenVPNConnection, len(servers))
for i := range servers {
connection := models.OpenVPNConnection{
IP: servers[i].IP,
Port: port,
Protocol: protocol,
Hostname: servers[i].Hostname,
}
connections[i] = connection
}
if selection.TargetIP != nil {
return utils.GetTargetIPConnection(connections, selection.TargetIP)
}
return utils.PickRandomConnection(connections, p.randSource), nil
}

View File

@@ -0,0 +1,28 @@
package privado
import (
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/provider/utils"
)
func (p *Privado) filterServers(selection configuration.ServerSelection) (
servers []models.PrivadoServer, err error) {
for _, server := range p.servers {
switch {
case
utils.FilterByPossibilities(server.Country, selection.Countries),
utils.FilterByPossibilities(server.Region, selection.Regions),
utils.FilterByPossibilities(server.City, selection.Cities),
utils.FilterByPossibilities(server.Hostname, selection.Hostnames):
default:
servers = append(servers, server)
}
}
if len(servers) == 0 {
return nil, utils.NoServerFoundError(selection)
}
return servers, nil
}

View File

@@ -0,0 +1,67 @@
package privado
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 (p *Privado) 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 10",
"ping-exit 60",
"ping-timer-rem",
"tls-exit",
// Privado specific
"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",
"verify-x509-name " + connection.Hostname + " name",
// Added constant values
"auth-nocache",
"mute-replay-warnings",
"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,
connection.ProtoLine(),
connection.RemoteLine(),
"data-ciphers-fallback " + settings.Cipher,
"data-ciphers " + settings.Cipher,
"auth " + settings.Auth,
}
if !settings.Root {
lines = append(lines, "user "+username)
}
if settings.MSSFix > 0 {
lines = append(lines, "mssfix "+strconv.Itoa(int(settings.MSSFix)))
}
lines = append(lines, utils.WrapOpenvpnCA(
constants.PrivadoCertificate)...)
lines = append(lines, "")
return lines
}

View File

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

View File

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

View File

@@ -0,0 +1,65 @@
package privateinternetaccess
import (
"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 (p *PIA) GetOpenVPNConnection(selection configuration.ServerSelection) (
connection models.OpenVPNConnection, err error) {
protocol := constants.UDP
if selection.TCP {
protocol = constants.TCP
}
port, err := getPort(selection.TCP, selection.EncryptionPreset, selection.CustomPort)
if err != nil {
return connection, err
}
servers, err := p.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,
}
connections = append(connections, connection)
}
}
if selection.TargetIP != nil {
connection, err = utils.GetTargetIPConnection(connections, selection.TargetIP)
} else {
connection, err = utils.PickRandomConnection(connections, p.randSource), nil
}
if err != nil {
return connection, err
}
p.activeServer = findActiveServer(servers, connection)
return connection, nil
}
func findActiveServer(servers []models.PIAServer,
connection models.OpenVPNConnection) (activeServer models.PIAServer) {
// Reverse lookup server using the randomly picked connection
for _, server := range servers {
for _, ip := range server.IPs {
if connection.IP.Equal(ip) {
return server
}
}
}
return activeServer
}

View File

@@ -0,0 +1,29 @@
package privateinternetaccess
import (
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/provider/utils"
)
func (p *PIA) filterServers(selection configuration.ServerSelection) (
servers []models.PIAServer, err error) {
for _, server := range p.servers {
switch {
case
utils.FilterByPossibilities(server.Region, selection.Regions),
utils.FilterByPossibilities(server.Hostname, selection.Hostnames),
utils.FilterByPossibilities(server.ServerName, selection.Names),
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,57 @@
package privateinternetaccess
import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"net"
"net/http"
"time"
"github.com/qdm12/gluetun/internal/constants"
)
var (
ErrParseCertificate = errors.New("cannot parse X509 certificate")
)
func newHTTPClient(serverName string) (client *http.Client, err error) {
certificateBytes, err := base64.StdEncoding.DecodeString(constants.PIACertificateStrong)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrParseCertificate, err)
}
certificate, err := x509.ParseCertificate(certificateBytes)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrParseCertificate, err)
}
//nolint:gomnd
transport := &http.Transport{
// Settings taken from http.DefaultTransport
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
rootCAs := x509.NewCertPool()
rootCAs.AddCert(certificate)
transport.TLSClientConfig = &tls.Config{
RootCAs: rootCAs,
MinVersion: tls.VersionTLS12,
ServerName: serverName,
}
const httpTimeout = 30 * time.Second
return &http.Client{
Transport: transport,
Timeout: httpTimeout,
}, nil
}

View File

@@ -0,0 +1,83 @@
package privateinternetaccess
import (
"strconv"
"strings"
"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 (p *PIA) BuildConf(connection models.OpenVPNConnection,
username string, settings configuration.OpenVPN) (lines []string) {
var defaultCipher, defaultAuth, X509CRL, certificate string
if settings.Provider.ExtraConfigOptions.EncryptionPreset == constants.PIAEncryptionPresetNormal {
defaultCipher = constants.AES128cbc
defaultAuth = constants.SHA1
X509CRL = constants.PiaX509CRLNormal
certificate = constants.PIACertificateNormal
} else { // strong encryption
defaultCipher = constants.AES256cbc
defaultAuth = constants.SHA256
X509CRL = constants.PiaX509CRLStrong
certificate = constants.PIACertificateStrong
}
if settings.Cipher == "" {
settings.Cipher = defaultCipher
}
if settings.Auth == "" {
settings.Auth = defaultAuth
}
lines = []string{
"client",
"dev tun",
"nobind",
"persist-key",
"remote-cert-tls server",
// PIA specific
"reneg-sec 0",
"disable-occ",
"compress", // allow PIA server to choose the compression to use
// Added constant values
"auth-nocache",
"mute-replay-warnings",
"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,
connection.ProtoLine(),
connection.RemoteLine(),
"data-ciphers-fallback " + settings.Cipher,
"data-ciphers " + settings.Cipher,
"auth " + settings.Auth,
}
if strings.HasSuffix(settings.Cipher, "-gcm") {
lines = append(lines, "ncp-disable")
}
if !settings.Root {
lines = append(lines, "user "+username)
}
if settings.MSSFix > 0 {
lines = append(lines, "mssfix "+strconv.Itoa(int(settings.MSSFix)))
}
lines = append(lines, utils.WrapOpenvpnCA(certificate)...)
lines = append(lines, utils.WrapOpenvpnCRLVerify(X509CRL)...)
lines = append(lines, "")
return lines
}

View File

@@ -0,0 +1,59 @@
package privateinternetaccess
import (
"errors"
"fmt"
"github.com/qdm12/gluetun/internal/constants"
)
func getPort(tcp bool, encryptionPreset string, customPort uint16) (
port uint16, err error) {
if customPort == 0 {
return getDefaultPort(tcp, encryptionPreset), nil
}
if err := checkPort(customPort, tcp); err != nil {
return 0, err
}
return customPort, nil
}
func getDefaultPort(tcp bool, encryptionPreset string) (port uint16) {
if tcp {
switch encryptionPreset {
case constants.PIAEncryptionPresetNormal:
port = 502
case constants.PIAEncryptionPresetStrong:
port = 501
}
} else {
switch encryptionPreset {
case constants.PIAEncryptionPresetNormal:
port = 1198
case constants.PIAEncryptionPresetStrong:
port = 1197
}
}
return port
}
var ErrInvalidPort = errors.New("invalid port number")
func checkPort(port uint16, tcp bool) (err error) {
if tcp {
switch port {
case 80, 110, 443: //nolint:gomnd
return nil
default:
return fmt.Errorf("%w: %d for protocol TCP", ErrInvalidPort, port)
}
}
switch port {
case 53, 1194, 1197, 1198, 8080, 9201: //nolint:gomnd
return nil
default:
return fmt.Errorf("%w: %d for protocol UDP", ErrInvalidPort, port)
}
}

View File

@@ -0,0 +1,508 @@
package privateinternetaccess
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/firewall"
format "github.com/qdm12/gluetun/internal/logging"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
var (
ErrBindPort = errors.New("cannot bind port")
)
//nolint:gocognit
func (p *PIA) PortForward(ctx context.Context, client *http.Client,
openFile os.OpenFileFunc, logger logging.Logger, gateway net.IP, fw firewall.Configurator,
syncState func(port uint16) (pfFilepath string)) {
defer logger.Warn("loop exited")
commonName := p.activeServer.ServerName
if !p.activeServer.PortForward {
logger.Error("The server " + commonName +
" (region " + p.activeServer.Region + ") does not support port forwarding")
return
}
if gateway == nil {
logger.Error("aborting because: VPN gateway IP address was not found")
return
}
privateIPClient, err := newHTTPClient(commonName)
if err != nil {
logger.Error("aborting because: " + err.Error())
return
}
data, err := readPIAPortForwardData(openFile)
if err != nil {
logger.Error(err)
}
dataFound := data.Port > 0
durationToExpiration := data.Expiration.Sub(p.timeNow())
expired := durationToExpiration <= 0
if dataFound {
logger.Info("Found persistent forwarded port data for port " + strconv.Itoa(int(data.Port)))
if expired {
logger.Warn("Forwarded port data expired on " +
data.Expiration.Format(time.RFC1123) + ", getting another one")
} else {
logger.Info("Forwarded port data expires in " + format.FormatDuration(durationToExpiration))
}
}
if !dataFound || expired {
tryUntilSuccessful(ctx, logger, func() error {
data, err = refreshPIAPortForwardData(ctx, client, privateIPClient, gateway, openFile)
return err
})
if ctx.Err() != nil {
return
}
durationToExpiration = data.Expiration.Sub(p.timeNow())
}
logger.Info("Port forwarded is " + strconv.Itoa(int(data.Port)) +
" expiring in " + format.FormatDuration(durationToExpiration))
// First time binding
tryUntilSuccessful(ctx, logger, func() error {
if err := bindPort(ctx, privateIPClient, gateway, data); err != nil {
return fmt.Errorf("%w: %s", ErrBindPort, err)
}
return nil
})
if ctx.Err() != nil {
return
}
filepath := syncState(data.Port)
logger.Info("Writing port to " + filepath)
if err := writePortForwardedToFile(openFile, filepath, data.Port); err != nil {
logger.Error(err)
}
if err := fw.SetAllowedPort(ctx, data.Port, string(constants.TUN)); err != nil {
logger.Error(err)
}
expiryTimer := time.NewTimer(durationToExpiration)
const keepAlivePeriod = 15 * time.Minute
// Timer behaving as a ticker
keepAliveTimer := time.NewTimer(keepAlivePeriod)
for {
select {
case <-ctx.Done():
removeCtx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if err := fw.RemoveAllowedPort(removeCtx, data.Port); err != nil {
logger.Error(err)
}
if !keepAliveTimer.Stop() {
<-keepAliveTimer.C
}
if !expiryTimer.Stop() {
<-expiryTimer.C
}
return
case <-keepAliveTimer.C:
if err := bindPort(ctx, privateIPClient, gateway, data); err != nil {
logger.Error("cannot bind port: " + err.Error())
}
keepAliveTimer.Reset(keepAlivePeriod)
case <-expiryTimer.C:
logger.Warn("Forward port has expired on " +
data.Expiration.Format(time.RFC1123) + ", getting another one")
oldPort := data.Port
for {
data, err = refreshPIAPortForwardData(ctx, client, privateIPClient, gateway, openFile)
if err != nil {
logger.Error(err)
continue
}
break
}
durationToExpiration := data.Expiration.Sub(p.timeNow())
logger.Info("Port forwarded is " + strconv.Itoa(int(data.Port)) +
" expiring in " + format.FormatDuration(durationToExpiration))
if err := fw.RemoveAllowedPort(ctx, oldPort); err != nil {
logger.Error(err)
}
if err := fw.SetAllowedPort(ctx, data.Port, string(constants.TUN)); err != nil {
logger.Error(err)
}
filepath := syncState(data.Port)
logger.Info("Writing port to " + filepath)
if err := writePortForwardedToFile(openFile, filepath, data.Port); err != nil {
logger.Error("Cannot write port forward data to file: " + err.Error())
}
if err := bindPort(ctx, privateIPClient, gateway, data); err != nil {
logger.Error("Cannot bind port: " + err.Error())
}
if !keepAliveTimer.Stop() {
<-keepAliveTimer.C
}
keepAliveTimer.Reset(keepAlivePeriod)
expiryTimer.Reset(durationToExpiration)
}
}
}
var (
ErrFetchToken = errors.New("cannot fetch token")
ErrFetchPortForwarding = errors.New("cannot fetch port forwarding data")
ErrPersistPortForwarding = errors.New("cannot persist port forwarding data")
)
func refreshPIAPortForwardData(ctx context.Context, client, privateIPClient *http.Client,
gateway net.IP, openFile os.OpenFileFunc) (data piaPortForwardData, err error) {
data.Token, err = fetchToken(ctx, openFile, client)
if err != nil {
return data, fmt.Errorf("%w: %s", ErrFetchToken, err)
}
data.Port, data.Signature, data.Expiration, err = fetchPortForwardData(ctx, privateIPClient, gateway, data.Token)
if err != nil {
return data, fmt.Errorf("%w: %s", ErrFetchPortForwarding, err)
}
if err := writePIAPortForwardData(openFile, data); err != nil {
return data, fmt.Errorf("%w: %s", ErrPersistPortForwarding, err)
}
return data, nil
}
type piaPayload struct {
Token string `json:"token"`
Port uint16 `json:"port"`
Expiration time.Time `json:"expires_at"`
}
type piaPortForwardData struct {
Port uint16 `json:"port"`
Token string `json:"token"`
Signature string `json:"signature"`
Expiration time.Time `json:"expires_at"`
}
func readPIAPortForwardData(openFile os.OpenFileFunc) (data piaPortForwardData, err error) {
file, err := openFile(constants.PIAPortForward, os.O_RDONLY, 0)
if os.IsNotExist(err) {
return data, nil
} else if err != nil {
return data, err
}
decoder := json.NewDecoder(file)
if err := decoder.Decode(&data); err != nil {
_ = file.Close()
return data, err
}
return data, file.Close()
}
func writePIAPortForwardData(openFile os.OpenFileFunc, data piaPortForwardData) (err error) {
file, err := openFile(constants.PIAPortForward, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
if err != nil {
return err
}
encoder := json.NewEncoder(file)
if err := encoder.Encode(data); err != nil {
_ = file.Close()
return err
}
return file.Close()
}
func unpackPayload(payload string) (port uint16, token string, expiration time.Time, err error) {
b, err := base64.StdEncoding.DecodeString(payload)
if err != nil {
return 0, "", expiration,
fmt.Errorf("%w: for payload: %s", err, payload)
}
var payloadData piaPayload
if err := json.Unmarshal(b, &payloadData); err != nil {
return 0, "", expiration,
fmt.Errorf("%w: for data: %s", err, string(b))
}
return payloadData.Port, payloadData.Token, payloadData.Expiration, nil
}
func packPayload(port uint16, token string, expiration time.Time) (payload string, err error) {
payloadData := piaPayload{
Token: token,
Port: port,
Expiration: expiration,
}
b, err := json.Marshal(&payloadData)
if err != nil {
return "", err
}
payload = base64.StdEncoding.EncodeToString(b)
return payload, nil
}
var (
errGetCredentials = errors.New("cannot get username and password")
errEmptyToken = errors.New("token received is empty")
)
func fetchToken(ctx context.Context, openFile os.OpenFileFunc,
client *http.Client) (token string, err error) {
username, password, err := getOpenvpnCredentials(openFile)
if err != nil {
return "", fmt.Errorf("%w: %s", errGetCredentials, err)
}
errSubstitutions := map[string]string{
username: "<username>",
password: "<password>",
}
url := url.URL{
Scheme: "https",
User: url.UserPassword(username, password),
Host: "privateinternetaccess.com",
Path: "/gtoken/generateToken",
}
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil)
if err != nil {
return "", replaceInErr(err, errSubstitutions)
}
response, err := client.Do(request)
if err != nil {
return "", replaceInErr(err, errSubstitutions)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return "", makeNOKStatusError(response, nil)
}
decoder := json.NewDecoder(response.Body)
var result struct {
Token string `json:"token"`
}
if err := decoder.Decode(&result); err != nil {
return "", fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
}
if result.Token == "" {
return "", errEmptyToken
}
return result.Token, nil
}
var (
errAuthFileRead = errors.New("cannot read OpenVPN authentication file")
errAuthFileMalformed = errors.New("authentication file is malformed")
)
func getOpenvpnCredentials(openFile os.OpenFileFunc) (username, password string, err error) {
file, err := openFile(constants.OpenVPNAuthConf, os.O_RDONLY, 0)
if err != nil {
return "", "", fmt.Errorf("%w: %s", errAuthFileRead, err)
}
authData, err := ioutil.ReadAll(file)
if err != nil {
_ = file.Close()
return "", "", fmt.Errorf("%w: %s", errAuthFileRead, err)
}
if err := file.Close(); err != nil {
return "", "", err
}
lines := strings.Split(string(authData), "\n")
const minLines = 2
if len(lines) < minLines {
return "", "", fmt.Errorf("%w: only %d lines exist", errAuthFileMalformed, len(lines))
}
username, password = lines[0], lines[1]
return username, password, nil
}
var (
errGetSignaturePayload = errors.New("cannot obtain signature payload")
errUnpackPayload = errors.New("cannot unpack payload data")
)
func fetchPortForwardData(ctx context.Context, client *http.Client, gateway net.IP, token string) (
port uint16, signature string, expiration time.Time, err error) {
errSubstitutions := map[string]string{token: "<token>"}
queryParams := new(url.Values)
queryParams.Add("token", token)
url := url.URL{
Scheme: "https",
Host: net.JoinHostPort(gateway.String(), "19999"),
Path: "/getSignature",
RawQuery: queryParams.Encode(),
}
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil)
if err != nil {
err = replaceInErr(err, errSubstitutions)
return 0, "", expiration, fmt.Errorf("%w: %s", errGetSignaturePayload, err)
}
response, err := client.Do(request)
if err != nil {
err = replaceInErr(err, errSubstitutions)
return 0, "", expiration, fmt.Errorf("%w: %s", errGetSignaturePayload, err)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return 0, "", expiration, makeNOKStatusError(response, errSubstitutions)
}
decoder := json.NewDecoder(response.Body)
var data struct {
Status string `json:"status"`
Payload string `json:"payload"`
Signature string `json:"signature"`
}
if err := decoder.Decode(&data); err != nil {
return 0, "", expiration, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
}
if data.Status != "OK" {
return 0, "", expiration, fmt.Errorf("%w: status is: %s", ErrBadResponse, data.Status)
}
port, _, expiration, err = unpackPayload(data.Payload)
if err != nil {
return 0, "", expiration, fmt.Errorf("%w: %s", errUnpackPayload, err)
}
return port, data.Signature, expiration, err
}
var (
ErrSerializePayload = errors.New("cannot serialize payload")
ErrUnmarshalResponse = errors.New("cannot unmarshal response")
ErrBadResponse = errors.New("bad response received")
)
func bindPort(ctx context.Context, client *http.Client, gateway net.IP, data piaPortForwardData) (err error) {
payload, err := packPayload(data.Port, data.Token, data.Expiration)
if err != nil {
return fmt.Errorf("%w: %s", ErrSerializePayload, err)
}
queryParams := new(url.Values)
queryParams.Add("payload", payload)
queryParams.Add("signature", data.Signature)
url := url.URL{
Scheme: "https",
Host: net.JoinHostPort(gateway.String(), "19999"),
Path: "/bindPort",
RawQuery: queryParams.Encode(),
}
errSubstitutions := map[string]string{
payload: "<payload>",
data.Signature: "<signature>",
}
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil)
if err != nil {
return replaceInErr(err, errSubstitutions)
}
response, err := client.Do(request)
if err != nil {
return replaceInErr(err, errSubstitutions)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return makeNOKStatusError(response, errSubstitutions)
}
decoder := json.NewDecoder(response.Body)
var responseData struct {
Status string `json:"status"`
Message string `json:"message"`
}
if err := decoder.Decode(&responseData); err != nil {
return fmt.Errorf("%w: from %s: %s", ErrUnmarshalResponse, url.String(), err)
}
if responseData.Status != "OK" {
return fmt.Errorf("%w: %s: %s", ErrBadResponse, responseData.Status, responseData.Message)
}
return nil
}
func writePortForwardedToFile(openFile os.OpenFileFunc,
filepath string, port uint16) (err error) {
file, err := openFile(filepath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
if err != nil {
return err
}
_, err = file.Write([]byte(fmt.Sprintf("%d", port)))
if err != nil {
_ = file.Close()
return err
}
return file.Close()
}
// replaceInErr is used to remove sensitive information from errors.
func replaceInErr(err error, substitutions map[string]string) error {
s := replaceInString(err.Error(), substitutions)
return errors.New(s)
}
// replaceInString is used to remove sensitive information.
func replaceInString(s string, substitutions map[string]string) string {
for old, new := range substitutions {
s = strings.ReplaceAll(s, old, new)
}
return s
}
var ErrHTTPStatusCodeNotOK = errors.New("HTTP status code is not OK")
func makeNOKStatusError(response *http.Response, substitutions map[string]string) (err error) {
url := response.Request.URL.String()
url = replaceInString(url, substitutions)
b, _ := ioutil.ReadAll(response.Body)
shortenMessage := string(b)
shortenMessage = strings.ReplaceAll(shortenMessage, "\n", "")
shortenMessage = strings.ReplaceAll(shortenMessage, " ", " ")
shortenMessage = replaceInString(shortenMessage, substitutions)
return fmt.Errorf("%w: %s: %s: response received: %s",
ErrHTTPStatusCodeNotOK, url, response.Status, shortenMessage)
}

View File

@@ -1,4 +1,4 @@
package provider
package privateinternetaccess
import (
"crypto/tls"
@@ -15,7 +15,7 @@ import (
"github.com/stretchr/testify/require"
)
func Test_newPIAHTTPClient(t *testing.T) {
func Test_newHTTPClient(t *testing.T) {
t.Parallel()
const serverName = "testserver"
@@ -35,7 +35,7 @@ func Test_newPIAHTTPClient(t *testing.T) {
ServerName: serverName,
}
piaClient, err := newPIAHTTPClient(serverName)
piaClient, err := newHTTPClient(serverName)
require.NoError(t, err)
@@ -47,7 +47,7 @@ func Test_newPIAHTTPClient(t *testing.T) {
assert.Equal(t, expectedPIATransportTLSConfig, piaTransport.TLSClientConfig)
}
func Test_unpackPIAPayload(t *testing.T) {
func Test_unpackPayload(t *testing.T) {
t.Parallel()
const exampleToken = "token"
@@ -70,11 +70,11 @@ func Test_unpackPIAPayload(t *testing.T) {
},
"invalid base64 payload": {
payload: "invalid",
err: errors.New(`cannot decode payload: payload is "invalid": illegal base64 data at input byte 4`),
err: errors.New("illegal base64 data at input byte 4: for payload: invalid"),
},
"invalid json payload": {
payload: base64.StdEncoding.EncodeToString([]byte{1}),
err: errors.New(`cannot parse payload data: data is "\x01": invalid character '\x01' looking for beginning of value`), //nolint:lll
err: errors.New("invalid character '\\x01' looking for beginning of value: for data: \x01"),
},
}
@@ -82,7 +82,7 @@ func Test_unpackPIAPayload(t *testing.T) {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
port, token, expiration, err := unpackPIAPayload(testCase.payload)
port, token, expiration, err := unpackPayload(testCase.payload)
if testCase.err != nil {
require.Error(t, err)
@@ -112,3 +112,32 @@ func makePIAPayload(t *testing.T, token string, port uint16, expiration time.Tim
return base64.StdEncoding.EncodeToString(b)
}
func Test_replaceInString(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
s string
substitutions map[string]string
result string
}{
"empty": {},
"multiple replacements": {
s: "https://test.com/username/password/",
substitutions: map[string]string{
"username": "xxx",
"password": "yyy",
},
result: "https://test.com/xxx/yyy/",
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
result := replaceInString(testCase.s, testCase.substitutions)
assert.Equal(t, testCase.result, result)
})
}
}

View File

@@ -0,0 +1,23 @@
package privateinternetaccess
import (
"math/rand"
"time"
"github.com/qdm12/gluetun/internal/models"
)
type PIA struct {
servers []models.PIAServer
randSource rand.Source
timeNow func() time.Time
activeServer models.PIAServer
}
func New(servers []models.PIAServer, randSource rand.Source, timeNow func() time.Time) *PIA {
return &PIA{
servers: servers,
timeNow: timeNow,
randSource: randSource,
}
}

View File

@@ -0,0 +1,31 @@
package privateinternetaccess
import (
"context"
"time"
"github.com/qdm12/golibs/logging"
)
func tryUntilSuccessful(ctx context.Context, logger logging.Logger, fn func() error) {
const initialRetryPeriod = 5 * time.Second
retryPeriod := initialRetryPeriod
for {
err := fn()
if err == nil {
break
}
logger.Error(err)
logger.Info("Trying again in " + retryPeriod.String())
timer := time.NewTimer(retryPeriod)
select {
case <-timer.C:
case <-ctx.Done():
if !timer.Stop() {
<-timer.C
}
return
}
retryPeriod *= 2
}
}

View File

@@ -1,172 +0,0 @@
package provider
import (
"context"
"fmt"
"math/rand"
"net"
"net/http"
"strconv"
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
type privatevpn struct {
servers []models.PrivatevpnServer
randSource rand.Source
}
func newPrivatevpn(servers []models.PrivatevpnServer, timeNow timeNowFunc) *privatevpn {
return &privatevpn{
servers: servers,
randSource: rand.NewSource(timeNow().UnixNano()),
}
}
func (p *privatevpn) filterServers(countries, cities, hostnames []string) (servers []models.PrivatevpnServer) {
for _, server := range p.servers {
switch {
case
filterByPossibilities(server.Country, countries),
filterByPossibilities(server.City, cities),
filterByPossibilities(server.Hostname, hostnames):
default:
servers = append(servers, server)
}
}
return servers
}
func (p *privatevpn) notFoundErr(selection configuration.ServerSelection) error {
message := "no server found for protocol " + tcpBoolToProtocol(selection.TCP)
if len(selection.Countries) > 0 {
message += " + countries " + commaJoin(selection.Countries)
}
if len(selection.Cities) > 0 {
message += " + cities " + commaJoin(selection.Cities)
}
if len(selection.Hostnames) > 0 {
message += " + hostnames " + commaJoin(selection.Hostnames)
}
return fmt.Errorf(message)
}
func (p *privatevpn) GetOpenVPNConnection(selection configuration.ServerSelection) (
connection models.OpenVPNConnection, err error) {
var port uint16
protocol := constants.TCP
if selection.TCP {
port = 443
} else {
protocol = constants.UDP
port = 1194
}
if selection.TargetIP != nil {
return models.OpenVPNConnection{
IP: selection.TargetIP,
Port: port,
Protocol: protocol,
}, nil
}
servers := p.filterServers(selection.Countries, selection.Cities, selection.Hostnames)
if len(servers) == 0 {
return connection, p.notFoundErr(selection)
}
var connections []models.OpenVPNConnection
for _, server := range servers {
for _, ip := range server.IPs {
connection := models.OpenVPNConnection{
IP: ip,
Port: port,
Protocol: protocol,
}
connections = append(connections, connection)
}
}
return pickRandomConnection(connections, p.randSource), nil
}
func (p *privatevpn) BuildConf(connection models.OpenVPNConnection,
username string, settings configuration.OpenVPN) (lines []string) {
if len(settings.Cipher) == 0 {
settings.Cipher = aes128gcm
}
if len(settings.Auth) == 0 {
settings.Auth = sha256
}
lines = []string{
"client",
"dev tun",
"nobind",
"persist-key",
"remote-cert-tls server",
"tls-exit",
// Privatevpn specific
"comp-lzo",
"tun-ipv6",
// Added constant values
"auth-nocache",
"mute-replay-warnings",
"pull-filter ignore \"auth-token\"", // prevent auth failed loops
"pull-filter ignore \"block-outside-dns\"",
"auth-retry nointeract",
"suppress-timestamps",
// Modified variables
fmt.Sprintf("verb %d", settings.Verbosity),
fmt.Sprintf("auth-user-pass %s", constants.OpenVPNAuthConf),
fmt.Sprintf("proto %s", connection.Protocol),
fmt.Sprintf("remote %s %d", connection.IP, connection.Port),
"data-ciphers-fallback " + settings.Cipher,
"data-ciphers " + settings.Cipher,
fmt.Sprintf("auth %s", settings.Auth),
}
if connection.Protocol == constants.UDP {
lines = append(lines, "key-direction 1")
}
if !settings.Root {
lines = append(lines, "user "+username)
}
if settings.MSSFix > 0 {
line := "mssfix " + strconv.Itoa(int(settings.MSSFix))
lines = append(lines, line)
}
lines = append(lines, []string{
"<ca>",
"-----BEGIN CERTIFICATE-----",
constants.PrivatevpnCertificate,
"-----END CERTIFICATE-----",
"</ca>",
}...)
lines = append(lines, []string{
"<tls-crypt>",
"-----BEGIN OpenVPN Static key V1-----",
constants.PrivatevpnOpenvpnStaticKeyV1,
"-----END OpenVPN Static key V1-----",
"</tls-crypt>",
"",
}...)
return lines
}
func (p *privatevpn) 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 privatevpn")
}

View File

@@ -0,0 +1,41 @@
package privatevpn
import (
"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 (p *Privatevpn) GetOpenVPNConnection(selection configuration.ServerSelection) (
connection models.OpenVPNConnection, err error) {
protocol := constants.UDP
var port uint16 = 1194
if selection.TCP {
protocol = constants.TCP
port = 443
}
servers, err := p.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,
}
connections = append(connections, connection)
}
}
if selection.TargetIP != nil {
return utils.GetTargetIPConnection(connections, selection.TargetIP)
}
return utils.PickRandomConnection(connections, p.randSource), nil
}

View File

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

View File

@@ -0,0 +1,71 @@
package privatevpn
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 (p *Privatevpn) BuildConf(connection models.OpenVPNConnection,
username string, settings configuration.OpenVPN) (lines []string) {
if settings.Cipher == "" {
settings.Cipher = constants.AES128gcm
}
if settings.Auth == "" {
settings.Auth = constants.SHA256
}
lines = []string{
"client",
"dev tun",
"nobind",
"persist-key",
"remote-cert-tls server",
"tls-exit",
// Privatevpn specific
"comp-lzo",
"tun-ipv6",
// Added constant values
"auth-nocache",
"mute-replay-warnings",
"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,
connection.ProtoLine(),
connection.RemoteLine(),
"data-ciphers-fallback " + settings.Cipher,
"data-ciphers " + settings.Cipher,
"auth " + settings.Auth,
}
if connection.Protocol == constants.UDP {
lines = append(lines, "key-direction 1")
}
if !settings.Root {
lines = append(lines, "user "+username)
}
if settings.MSSFix > 0 {
lines = append(lines, "mssfix "+strconv.Itoa(int(settings.MSSFix)))
}
lines = append(lines, utils.WrapOpenvpnCA(
constants.PrivatevpnCertificate)...)
lines = append(lines, utils.WrapOpenvpnTLSCrypt(
constants.PrivatevpnOpenvpnStaticKeyV1)...)
lines = append(lines, "")
return lines
}

View File

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

View File

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

View File

@@ -1,210 +0,0 @@
package provider
import (
"context"
"fmt"
"math/rand"
"net"
"net/http"
"strconv"
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
type protonvpn struct {
servers []models.ProtonvpnServer
randSource rand.Source
}
func newProtonvpn(servers []models.ProtonvpnServer, timeNow timeNowFunc) *protonvpn {
return &protonvpn{
servers: servers,
randSource: rand.NewSource(timeNow().UnixNano()),
}
}
func (p *protonvpn) GetOpenVPNConnection(selection configuration.ServerSelection) (
connection models.OpenVPNConnection, err error) {
port, err := p.getPort(selection)
if err != nil {
return connection, err
}
protocol := tcpBoolToProtocol(selection.TCP)
if selection.TargetIP != nil {
return models.OpenVPNConnection{
IP: selection.TargetIP,
Port: port,
Protocol: protocol,
}, nil
}
servers := p.filterServers(selection.Countries, selection.Regions,
selection.Cities, selection.Names, selection.Hostnames)
if len(servers) == 0 {
return connection, p.notFoundErr(selection)
}
connections := make([]models.OpenVPNConnection, len(servers))
for i := range servers {
connections[i] = models.OpenVPNConnection{
IP: servers[i].EntryIP,
Port: port,
Protocol: protocol,
}
}
return pickRandomConnection(connections, p.randSource), nil
}
func (p *protonvpn) BuildConf(connection models.OpenVPNConnection,
username string, settings configuration.OpenVPN) (lines []string) {
if len(settings.Cipher) == 0 {
settings.Cipher = aes256cbc
}
if len(settings.Auth) == 0 {
settings.Auth = "SHA512"
}
const defaultMSSFix = 1450
if settings.MSSFix == 0 {
settings.MSSFix = defaultMSSFix
}
lines = []string{
"client",
"dev tun",
"nobind",
"persist-key",
"remote-cert-tls server",
"tls-exit",
// Protonvpn specific
"tun-mtu 1500",
"tun-mtu-extra 32",
"mssfix " + strconv.Itoa(int(settings.MSSFix)),
"reneg-sec 0",
"fast-io",
"key-direction 1",
"pull",
"comp-lzo no",
// Added constant values
"auth-nocache",
"mute-replay-warnings",
"pull-filter ignore \"auth-token\"", // prevent auth failed loops
"pull-filter ignore \"block-outside-dns\"",
`pull-filter ignore "ping-restart"`,
"auth-retry nointeract",
"suppress-timestamps",
// Modified variables
"verb " + strconv.Itoa(settings.Verbosity),
"auth-user-pass " + constants.OpenVPNAuthConf,
"proto " + connection.Protocol,
"remote " + connection.IP.String() + " " + strconv.Itoa(int(connection.Port)),
"cipher " + settings.Cipher,
"auth " + settings.Auth,
}
if !settings.Root {
lines = append(lines, "user "+username)
}
lines = append(lines, []string{
"<ca>",
"-----BEGIN CERTIFICATE-----",
constants.ProtonvpnCertificate,
"-----END CERTIFICATE-----",
"</ca>",
}...)
lines = append(lines, []string{
"<tls-auth>",
"-----BEGIN OpenVPN Static key V1-----",
constants.ProtonvpnOpenvpnStaticKeyV1,
"-----END OpenVPN Static key V1-----",
"</tls-auth>",
"",
}...)
return lines
}
func (p *protonvpn) 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 protonvpn")
}
func (p *protonvpn) getPort(selection configuration.ServerSelection) (port uint16, err error) {
if selection.CustomPort == 0 {
if selection.TCP {
const defaultTCPPort = 443
return defaultTCPPort, nil
}
const defaultUDPPort = 1194
return defaultUDPPort, nil
}
port = selection.CustomPort
if selection.TCP {
switch port {
case 443, 5995, 8443: //nolint:gomnd
return port, nil
default:
return 0, fmt.Errorf("%w: %d for protocol TCP", ErrInvalidPort, port)
}
}
switch port {
case 80, 443, 1194, 4569, 5060: //nolint:gomnd
return port, nil
default:
return 0, fmt.Errorf("%w: %d for protocol UDP", ErrInvalidPort, port)
}
}
func (p *protonvpn) filterServers(countries, regions, cities, names, hostnames []string) (
servers []models.ProtonvpnServer) {
for _, server := range p.servers {
switch {
case
filterByPossibilities(server.Country, countries),
filterByPossibilities(server.Region, regions),
filterByPossibilities(server.City, cities),
filterByPossibilities(server.Name, names),
filterByPossibilities(server.Hostname, hostnames):
default:
servers = append(servers, server)
}
}
return servers
}
func (p *protonvpn) notFoundErr(selection configuration.ServerSelection) error {
message := "no server found for protocol " + tcpBoolToProtocol(selection.TCP)
if len(selection.Countries) > 0 {
message += " + countries " + commaJoin(selection.Countries)
}
if len(selection.Regions) > 0 {
message += " + regions " + commaJoin(selection.Regions)
}
if len(selection.Cities) > 0 {
message += " + cities " + commaJoin(selection.Cities)
}
if len(selection.Names) > 0 {
message += " + names " + commaJoin(selection.Names)
}
if len(selection.Hostnames) > 0 {
message += " + hostnames " + commaJoin(selection.Hostnames)
}
return fmt.Errorf(message)
}

View File

@@ -0,0 +1,41 @@
package protonvpn
import (
"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 (p *Protonvpn) GetOpenVPNConnection(selection configuration.ServerSelection) (
connection models.OpenVPNConnection, err error) {
protocol := constants.UDP
if selection.TCP {
protocol = constants.TCP
}
port, err := getPort(selection.TCP, selection.CustomPort)
if err != nil {
return connection, err
}
servers, err := p.filterServers(selection)
if err != nil {
return connection, err
}
connections := make([]models.OpenVPNConnection, len(servers))
for i := range servers {
connections[i] = models.OpenVPNConnection{
IP: servers[i].EntryIP,
Port: port,
Protocol: protocol,
}
}
if selection.TargetIP != nil {
return utils.GetTargetIPConnection(connections, selection.TargetIP)
}
return utils.PickRandomConnection(connections, p.randSource), nil
}

View File

@@ -0,0 +1,29 @@
package protonvpn
import (
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/provider/utils"
)
func (p *Protonvpn) filterServers(selection configuration.ServerSelection) (
servers []models.ProtonvpnServer, err error) {
for _, server := range p.servers {
switch {
case
utils.FilterByPossibilities(server.Country, selection.Countries),
utils.FilterByPossibilities(server.Region, selection.Regions),
utils.FilterByPossibilities(server.City, selection.Cities),
utils.FilterByPossibilities(server.Hostname, selection.Hostnames),
utils.FilterByPossibilities(server.Name, selection.Names):
default:
servers = append(servers, server)
}
}
if len(servers) == 0 {
return nil, utils.NoServerFoundError(selection)
}
return servers, nil
}

View File

@@ -0,0 +1,74 @@
package protonvpn
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 (p *Protonvpn) BuildConf(connection models.OpenVPNConnection,
username string, settings configuration.OpenVPN) (lines []string) {
if settings.Cipher == "" {
settings.Cipher = constants.AES256cbc
}
if settings.Auth == "" {
settings.Auth = constants.SHA512
}
const defaultMSSFix = 1450
if settings.MSSFix == 0 {
settings.MSSFix = defaultMSSFix
}
lines = []string{
"client",
"dev tun",
"nobind",
"persist-key",
"remote-cert-tls server",
"tls-exit",
// Protonvpn specific
"tun-mtu 1500",
"tun-mtu-extra 32",
"mssfix " + strconv.Itoa(int(settings.MSSFix)),
"reneg-sec 0",
"fast-io",
"key-direction 1",
"pull",
"comp-lzo no",
// Added constant values
"auth-nocache",
"mute-replay-warnings",
"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,
connection.ProtoLine(),
connection.RemoteLine(),
"data-ciphers-fallback " + settings.Cipher,
"data-ciphers " + settings.Cipher,
"auth " + settings.Auth,
}
if !settings.Root {
lines = append(lines, "user "+username)
}
lines = append(lines, utils.WrapOpenvpnCA(
constants.ProtonvpnCertificate)...)
lines = append(lines, utils.WrapOpenvpnTLSAuth(
constants.ProtonvpnOpenvpnStaticKeyV1)...)
lines = append(lines, "")
return lines
}

View File

@@ -0,0 +1,41 @@
package protonvpn
import (
"errors"
"fmt"
)
func getPort(tcp bool, customPort uint16) (port uint16, err error) {
if customPort == 0 {
const defaultTCPPort, defaultUDPPort = 443, 1194
if tcp {
return defaultTCPPort, nil
}
return defaultUDPPort, nil
}
if err := checkPort(customPort, tcp); err != nil {
return 0, err
}
return customPort, nil
}
var ErrInvalidPort = errors.New("invalid port number")
func checkPort(port uint16, tcp bool) (err error) {
if tcp {
switch port {
case 443, 5995, 8443: //nolint:gomnd
return nil
default:
return fmt.Errorf("%w: %d for protocol TCP", ErrInvalidPort, port)
}
}
switch port {
case 80, 443, 1194, 4569, 5060: //nolint:gomnd
return nil
default:
return fmt.Errorf("%w: %d for protocol UDP", ErrInvalidPort, port)
}
}

View File

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

View File

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

View File

@@ -3,13 +3,29 @@ package provider
import (
"context"
"math/rand"
"net"
"net/http"
"time"
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/provider/cyberghost"
"github.com/qdm12/gluetun/internal/provider/fastestvpn"
"github.com/qdm12/gluetun/internal/provider/hidemyass"
"github.com/qdm12/gluetun/internal/provider/mullvad"
"github.com/qdm12/gluetun/internal/provider/nordvpn"
"github.com/qdm12/gluetun/internal/provider/privado"
"github.com/qdm12/gluetun/internal/provider/privateinternetaccess"
"github.com/qdm12/gluetun/internal/provider/privatevpn"
"github.com/qdm12/gluetun/internal/provider/protonvpn"
"github.com/qdm12/gluetun/internal/provider/purevpn"
"github.com/qdm12/gluetun/internal/provider/surfshark"
"github.com/qdm12/gluetun/internal/provider/torguard"
"github.com/qdm12/gluetun/internal/provider/vyprvpn"
"github.com/qdm12/gluetun/internal/provider/windscribe"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
@@ -23,36 +39,37 @@ type Provider interface {
syncState func(port uint16) (pfFilepath string))
}
func New(provider string, allServers models.AllServers, timeNow timeNowFunc) Provider {
func New(provider string, allServers models.AllServers, timeNow func() time.Time) Provider {
randSource := rand.NewSource(timeNow().UnixNano())
switch provider {
case constants.Cyberghost:
return newCyberghost(allServers.Cyberghost.Servers, timeNow)
return cyberghost.New(allServers.Cyberghost.Servers, randSource)
case constants.Fastestvpn:
return newFastestvpn(allServers.Fastestvpn.Servers, timeNow)
return fastestvpn.New(allServers.Fastestvpn.Servers, randSource)
case constants.HideMyAss:
return newHideMyAss(allServers.HideMyAss.Servers, timeNow)
return hidemyass.New(allServers.HideMyAss.Servers, randSource)
case constants.Mullvad:
return newMullvad(allServers.Mullvad.Servers, timeNow)
return mullvad.New(allServers.Mullvad.Servers, randSource)
case constants.Nordvpn:
return newNordvpn(allServers.Nordvpn.Servers, timeNow)
return nordvpn.New(allServers.Nordvpn.Servers, randSource)
case constants.Privado:
return newPrivado(allServers.Privado.Servers, timeNow)
return privado.New(allServers.Privado.Servers, randSource)
case constants.PrivateInternetAccess:
return newPrivateInternetAccess(allServers.Pia.Servers, timeNow)
return privateinternetaccess.New(allServers.Pia.Servers, randSource, timeNow)
case constants.Privatevpn:
return newPrivatevpn(allServers.Privatevpn.Servers, timeNow)
return privatevpn.New(allServers.Privatevpn.Servers, randSource)
case constants.Protonvpn:
return newProtonvpn(allServers.Protonvpn.Servers, timeNow)
return protonvpn.New(allServers.Protonvpn.Servers, randSource)
case constants.Purevpn:
return newPurevpn(allServers.Purevpn.Servers, timeNow)
return purevpn.New(allServers.Purevpn.Servers, randSource)
case constants.Surfshark:
return newSurfshark(allServers.Surfshark.Servers, timeNow)
return surfshark.New(allServers.Surfshark.Servers, randSource)
case constants.Torguard:
return newTorguard(allServers.Torguard.Servers, timeNow)
return torguard.New(allServers.Torguard.Servers, randSource)
case constants.Vyprvpn:
return newVyprvpn(allServers.Vyprvpn.Servers, timeNow)
return vyprvpn.New(allServers.Vyprvpn.Servers, randSource)
case constants.Windscribe:
return newWindscribe(allServers.Windscribe.Servers, timeNow)
return windscribe.New(allServers.Windscribe.Servers, randSource)
default:
return nil // should never occur
}

View File

@@ -1,167 +0,0 @@
package provider
import (
"context"
"fmt"
"math/rand"
"net"
"net/http"
"strconv"
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
type purevpn struct {
servers []models.PurevpnServer
randSource rand.Source
}
func newPurevpn(servers []models.PurevpnServer, timeNow timeNowFunc) *purevpn {
return &purevpn{
servers: servers,
randSource: rand.NewSource(timeNow().UnixNano()),
}
}
func (p *purevpn) filterServers(regions, countries, cities, hostnames []string,
tcp bool) (servers []models.PurevpnServer) {
for _, server := range p.servers {
switch {
case
filterByPossibilities(server.Region, regions),
filterByPossibilities(server.Country, countries),
filterByPossibilities(server.City, cities),
filterByPossibilities(server.Hostname, hostnames),
tcp && !server.TCP,
!tcp && !server.UDP:
default:
servers = append(servers, server)
}
}
return servers
}
func (p *purevpn) GetOpenVPNConnection(selection configuration.ServerSelection) (
connection models.OpenVPNConnection, err error) {
var port uint16 = 53
protocol := constants.UDP
if selection.TCP {
port = 80
protocol = constants.TCP
}
if selection.TargetIP != nil {
return models.OpenVPNConnection{IP: selection.TargetIP, Port: port, Protocol: protocol}, nil
}
servers := p.filterServers(selection.Regions, selection.Countries,
selection.Cities, selection.Hostnames, selection.TCP)
if len(servers) == 0 {
return connection, fmt.Errorf("no server found for regions %s, countries %s and cities %s",
commaJoin(selection.Regions), commaJoin(selection.Countries), commaJoin(selection.Cities))
}
var connections []models.OpenVPNConnection
for _, server := range servers {
for _, IP := range server.IPs {
connections = append(connections, models.OpenVPNConnection{IP: IP, Port: port, Protocol: protocol})
}
}
return pickRandomConnection(connections, p.randSource), nil
}
func (p *purevpn) BuildConf(connection models.OpenVPNConnection,
username string, settings configuration.OpenVPN) (lines []string) {
if len(settings.Cipher) == 0 {
settings.Cipher = aes256cbc
}
lines = []string{
"client",
"dev tun",
"nobind",
"persist-key",
"remote-cert-tls server",
"ping 10",
"ping-exit 60",
"ping-timer-rem",
"tls-exit",
// Purevpn specific
"key-direction 1",
"remote-cert-tls server",
"cipher AES-256-CBC",
"route-method exe",
"route-delay 0",
"script-security 2",
// Added constant values
"auth-nocache",
"mute-replay-warnings",
"pull-filter ignore \"auth-token\"", // prevent auth failed loops
"auth-retry nointeract",
"suppress-timestamps",
// Modified variables
fmt.Sprintf("verb %d", settings.Verbosity),
fmt.Sprintf("auth-user-pass %s", constants.OpenVPNAuthConf),
fmt.Sprintf("proto %s", connection.Protocol),
fmt.Sprintf("remote %s %d", connection.IP.String(), connection.Port),
"data-ciphers-fallback " + settings.Cipher,
"data-ciphers " + settings.Cipher,
}
if !settings.Root {
lines = append(lines, "user "+username)
}
if settings.MSSFix > 0 {
lines = append(lines, "mssfix "+strconv.Itoa(int(settings.MSSFix)))
}
lines = append(lines, []string{
"<ca>",
"-----BEGIN CERTIFICATE-----",
constants.PurevpnCertificateAuthority,
"-----END CERTIFICATE-----",
"</ca>",
}...)
lines = append(lines, []string{
"<cert>",
"-----BEGIN CERTIFICATE-----",
constants.PurevpnCertificate,
"-----END CERTIFICATE-----",
"</cert>",
}...)
lines = append(lines, []string{
"<key>",
"-----BEGIN PRIVATE KEY-----",
constants.PurevpnKey,
"-----END PRIVATE KEY-----",
"</key>",
"",
}...)
lines = append(lines, []string{
"<tls-auth>",
"-----BEGIN OpenVPN Static key V1-----",
constants.PurevpnOpenvpnStaticKeyV1,
"-----END OpenVPN Static key V1-----",
"</tls-auth>",
"",
}...)
if len(settings.Auth) > 0 {
lines = append(lines, "auth "+settings.Auth)
}
if connection.Protocol == constants.UDP {
lines = append(lines, "explicit-exit-notify")
}
return lines
}
func (p *purevpn) 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 purevpn")
}

View File

@@ -0,0 +1,41 @@
package purevpn
import (
"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 (p *Purevpn) GetOpenVPNConnection(selection configuration.ServerSelection) (
connection models.OpenVPNConnection, err error) {
protocol := constants.UDP
var port uint16 = 53
if selection.TCP {
protocol = constants.TCP
port = 80
}
servers, err := p.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,
}
connections = append(connections, connection)
}
}
if selection.TargetIP != nil {
return utils.GetTargetIPConnection(connections, selection.TargetIP)
}
return utils.PickRandomConnection(connections, p.randSource), nil
}

View File

@@ -0,0 +1,30 @@
package purevpn
import (
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/provider/utils"
)
func (p *Purevpn) filterServers(selection configuration.ServerSelection) (
servers []models.PurevpnServer, err error) {
for _, server := range p.servers {
switch {
case
utils.FilterByPossibilities(server.Region, selection.Regions),
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,81 @@
package purevpn
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 (p *Purevpn) BuildConf(connection models.OpenVPNConnection,
username string, settings configuration.OpenVPN) (lines []string) {
if settings.Cipher == "" {
settings.Cipher = constants.AES256cbc
}
lines = []string{
"client",
"dev tun",
"nobind",
"persist-key",
"remote-cert-tls server",
"ping 10",
"ping-exit 60",
"ping-timer-rem",
"tls-exit",
// Purevpn specific
"key-direction 1",
"remote-cert-tls server",
"cipher AES-256-CBC",
"route-method exe",
"route-delay 0",
"script-security 2",
// Added constant values
"auth-nocache",
"mute-replay-warnings",
"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,
connection.ProtoLine(),
connection.RemoteLine(),
"data-ciphers-fallback " + settings.Cipher,
"data-ciphers " + settings.Cipher,
}
if connection.Protocol == constants.UDP {
lines = append(lines, "explicit-exit-notify")
}
if settings.Auth != "" {
lines = append(lines, "auth "+settings.Auth)
}
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.PurevpnCertificateAuthority)...)
lines = append(lines, utils.WrapOpenvpnCert(
constants.PurevpnCertificate)...)
lines = append(lines, utils.WrapOpenvpnKey(
constants.PurevpnKey)...)
lines = append(lines, utils.WrapOpenvpnTLSAuth(
constants.PurevpnOpenvpnStaticKeyV1)...)
lines = append(lines, "")
return lines
}

View File

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

View File

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

View File

@@ -1,168 +0,0 @@
package provider
import (
"context"
"fmt"
"math/rand"
"net"
"net/http"
"strconv"
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
type surfshark struct {
servers []models.SurfsharkServer
randSource rand.Source
}
func newSurfshark(servers []models.SurfsharkServer, timeNow timeNowFunc) *surfshark {
return &surfshark{
servers: servers,
randSource: rand.NewSource(timeNow().UnixNano()),
}
}
func (s *surfshark) filterServers(regions, hostnames []string, tcp bool) (servers []models.SurfsharkServer) {
for _, server := range s.servers {
switch {
case
filterByPossibilities(server.Region, regions),
filterByPossibilities(server.Hostname, hostnames),
tcp && !server.TCP,
!tcp && !server.UDP:
default:
servers = append(servers, server)
}
}
return servers
}
func (s *surfshark) notFoundErr(selection configuration.ServerSelection) error {
message := "for protocol " + tcpBoolToProtocol(selection.TCP)
if len(selection.Countries) > 0 {
message += " + regions " + commaJoin(selection.Regions)
}
if len(selection.Hostnames) > 0 {
message += " + hostnames " + commaJoin(selection.Hostnames)
}
return fmt.Errorf("%w: %s", errNoServerFound, message)
}
func (s *surfshark) GetOpenVPNConnection(selection configuration.ServerSelection) (
connection models.OpenVPNConnection, err error) {
var port uint16 = 1194
protocol := constants.UDP
if selection.TCP {
port = 1443
protocol = constants.TCP
}
if selection.TargetIP != nil {
return models.OpenVPNConnection{IP: selection.TargetIP, Port: port, Protocol: protocol}, nil
}
servers := s.filterServers(selection.Regions, selection.Hostnames, selection.TCP)
if len(servers) == 0 {
return connection, s.notFoundErr(selection)
}
var connections []models.OpenVPNConnection
for _, server := range servers {
for _, IP := range server.IPs {
connections = append(connections, models.OpenVPNConnection{IP: IP, Port: port, Protocol: protocol})
}
}
if selection.TargetIP != nil {
return connection, fmt.Errorf("target IP %s not found in IP addresses", selection.TargetIP)
}
return pickRandomConnection(connections, s.randSource), nil
}
func (s *surfshark) BuildConf(connection models.OpenVPNConnection,
username string, settings configuration.OpenVPN) (lines []string) {
if len(settings.Cipher) == 0 {
settings.Cipher = aes256gcm
}
if len(settings.Auth) == 0 {
settings.Auth = "SHA512"
}
const defaultMSSFix = 1450
if settings.MSSFix == 0 {
settings.MSSFix = defaultMSSFix
}
lines = []string{
"client",
"dev tun",
"nobind",
"persist-key",
"remote-cert-tls server",
"ping 15",
"ping-timer-rem",
"tls-exit",
// Surfshark specific
"tun-mtu 1500",
"tun-mtu-extra 32",
"mssfix " + strconv.Itoa(int(settings.MSSFix)),
"reneg-sec 0",
"fast-io",
"key-direction 1",
"script-security 2",
"ping-restart 0",
// Added constant values
"auth-nocache",
"mute-replay-warnings",
"pull-filter ignore \"auth-token\"", // prevent auth failed loops
"pull-filter ignore \"block-outside-dns\"",
"auth-retry nointeract",
"suppress-timestamps",
// Modified variables
fmt.Sprintf("verb %d", settings.Verbosity),
fmt.Sprintf("auth-user-pass %s", constants.OpenVPNAuthConf),
fmt.Sprintf("proto %s", connection.Protocol),
fmt.Sprintf("remote %s %d", connection.IP, connection.Port),
"data-ciphers-fallback " + settings.Cipher,
"data-ciphers " + settings.Cipher,
fmt.Sprintf("auth %s", settings.Auth),
}
if !settings.Root {
lines = append(lines, "user "+username)
}
lines = append(lines, []string{
"<ca>",
"-----BEGIN CERTIFICATE-----",
constants.SurfsharkCertificate,
"-----END CERTIFICATE-----",
"</ca>",
}...)
lines = append(lines, []string{
"<tls-auth>",
"-----BEGIN OpenVPN Static key V1-----",
constants.SurfsharkOpenvpnStaticKeyV1,
"-----END OpenVPN Static key V1-----",
"</tls-auth>",
"",
}...)
return lines
}
func (s *surfshark) 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 surfshark")
}

View File

@@ -0,0 +1,41 @@
package surfshark
import (
"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 (s *Surfshark) GetOpenVPNConnection(selection configuration.ServerSelection) (
connection models.OpenVPNConnection, err error) {
protocol := constants.UDP
var port uint16 = 1194
if selection.TCP {
protocol = constants.TCP
port = 1443
}
servers, err := s.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,
}
connections = append(connections, connection)
}
}
if selection.TargetIP != nil {
return utils.GetTargetIPConnection(connections, selection.TargetIP)
}
return utils.PickRandomConnection(connections, s.randSource), nil
}

View File

@@ -0,0 +1,28 @@
package surfshark
import (
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/provider/utils"
)
func (s *Surfshark) filterServers(selection configuration.ServerSelection) (
servers []models.SurfsharkServer, err error) {
for _, server := range s.servers {
switch {
case
utils.FilterByPossibilities(server.Region, selection.Regions),
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,76 @@
package surfshark
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 (s *Surfshark) BuildConf(connection models.OpenVPNConnection,
username string, settings configuration.OpenVPN) (lines []string) {
if settings.Cipher == "" {
settings.Cipher = constants.AES256gcm
}
if settings.Auth == "" {
settings.Auth = constants.SHA512
}
const defaultMSSFix = 1450
if settings.MSSFix == 0 {
settings.MSSFix = defaultMSSFix
}
lines = []string{
"client",
"dev tun",
"nobind",
"persist-key",
"remote-cert-tls server",
"ping 15",
"ping-timer-rem",
"tls-exit",
// Surfshark specific
"tun-mtu 1500",
"tun-mtu-extra 32",
"mssfix " + strconv.Itoa(int(settings.MSSFix)),
"reneg-sec 0",
"fast-io",
"key-direction 1",
"script-security 2",
"ping-restart 0",
// Added constant values
"auth-nocache",
"mute-replay-warnings",
"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,
connection.ProtoLine(),
connection.RemoteLine(),
"data-ciphers-fallback " + settings.Cipher,
"data-ciphers " + settings.Cipher,
"auth " + settings.Auth,
}
if !settings.Root {
lines = append(lines, "user "+username)
}
lines = append(lines, utils.WrapOpenvpnCA(
constants.SurfsharkCertificate)...)
lines = append(lines, utils.WrapOpenvpnTLSAuth(
constants.SurfsharkOpenvpnStaticKeyV1)...)
lines = append(lines, "")
return lines
}

View File

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

View File

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

View File

@@ -1,184 +0,0 @@
package provider
import (
"context"
"fmt"
"math/rand"
"net"
"net/http"
"strconv"
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
type torguard struct {
servers []models.TorguardServer
randSource rand.Source
}
func newTorguard(servers []models.TorguardServer, timeNow timeNowFunc) *torguard {
return &torguard{
servers: servers,
randSource: rand.NewSource(timeNow().UnixNano()),
}
}
func (t *torguard) filterServers(countries, cities, hostnames []string,
tcp bool) (servers []models.TorguardServer) {
for _, server := range t.servers {
switch {
case
filterByPossibilities(server.Country, countries),
filterByPossibilities(server.City, cities),
filterByPossibilities(server.Hostname, hostnames),
tcp && !server.TCP,
!tcp && !server.UDP:
default:
servers = append(servers, server)
}
}
return servers
}
func (t *torguard) notFoundErr(selection configuration.ServerSelection) error {
message := "no server found for protocol " + tcpBoolToProtocol(selection.TCP)
if len(selection.Countries) > 0 {
message += " + countries " + commaJoin(selection.Countries)
}
if len(selection.Cities) > 0 {
message += " + cities " + commaJoin(selection.Cities)
}
if len(selection.Hostnames) > 0 {
message += " + hostnames " + commaJoin(selection.Hostnames)
}
return fmt.Errorf(message)
}
func (t *torguard) GetOpenVPNConnection(selection configuration.ServerSelection) (
connection models.OpenVPNConnection, err error) {
var port uint16 = 1912
if selection.CustomPort > 0 {
port = selection.CustomPort
}
protocol := tcpBoolToProtocol(selection.TCP)
if selection.TargetIP != nil {
return models.OpenVPNConnection{IP: selection.TargetIP, Port: port, Protocol: protocol}, nil
}
servers := t.filterServers(selection.Countries, selection.Cities,
selection.Hostnames, selection.TCP)
if len(servers) == 0 {
return connection, t.notFoundErr(selection)
}
var connections []models.OpenVPNConnection
for _, server := range servers {
for _, ip := range server.IPs {
connection := models.OpenVPNConnection{
IP: ip,
Port: port,
Protocol: protocol,
}
connections = append(connections, connection)
}
}
return pickRandomConnection(connections, t.randSource), nil
}
func (t *torguard) BuildConf(connection models.OpenVPNConnection,
username string, settings configuration.OpenVPN) (lines []string) {
if len(settings.Cipher) == 0 {
settings.Cipher = aes256gcm
}
if len(settings.Auth) == 0 {
settings.Auth = sha256
}
const defaultMSSFix = 1450
if settings.MSSFix == 0 {
settings.MSSFix = defaultMSSFix
}
lines = []string{
"client",
"dev tun",
"nobind",
"persist-key",
"remote-cert-tls server",
"tls-exit",
// Torguard specific
"tun-mtu 1500",
"tun-mtu-extra 32",
"mssfix " + strconv.Itoa(int(settings.MSSFix)),
"reneg-sec 0",
"fast-io",
"key-direction 1",
"script-security 2",
"ncp-disable",
"compress",
"keepalive 5 30",
"sndbuf 393216",
"rcvbuf 393216",
// "up /etc/openvpn/update-resolv-conf",
// "down /etc/openvpn/update-resolv-conf",
// Added constant values
"auth-nocache",
"mute-replay-warnings",
"pull-filter ignore \"auth-token\"", // prevent auth failed loops
"pull-filter ignore \"block-outside-dns\"",
"auth-retry nointeract",
"suppress-timestamps",
// Modified variables
"verb " + strconv.Itoa(settings.Verbosity),
"auth-user-pass " + constants.OpenVPNAuthConf,
"proto " + connection.Protocol,
"remote " + connection.IP.String() + " " + strconv.Itoa(int(connection.Port)),
"data-ciphers-fallback " + settings.Cipher,
"data-ciphers " + settings.Cipher,
"auth " + settings.Auth,
}
if !settings.Root {
lines = append(lines, "user "+username)
}
lines = append(lines, []string{
"<ca>",
"-----BEGIN CERTIFICATE-----",
constants.TorguardCertificate,
"-----END CERTIFICATE-----",
"</ca>",
}...)
lines = append(lines, []string{
"<tls-auth>",
"-----BEGIN OpenVPN Static key V1-----",
constants.TorguardOpenvpnStaticKeyV1,
"-----END OpenVPN Static key V1-----",
"</tls-auth>",
"",
}...)
return lines
}
func (t *torguard) 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 torguard")
}

View File

@@ -0,0 +1,44 @@
package torguard
import (
"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 (t *Torguard) GetOpenVPNConnection(selection configuration.ServerSelection) (
connection models.OpenVPNConnection, err error) {
protocol := constants.UDP
if selection.TCP {
protocol = constants.TCP
}
var port uint16 = 1912
if selection.CustomPort > 0 {
port = selection.CustomPort
}
servers, err := t.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,
}
connections = append(connections, connection)
}
}
if selection.TargetIP != nil {
return utils.GetTargetIPConnection(connections, selection.TargetIP)
}
return utils.PickRandomConnection(connections, t.randSource), nil
}

View File

@@ -0,0 +1,29 @@
package torguard
import (
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/provider/utils"
)
func (t *Torguard) filterServers(selection configuration.ServerSelection) (
servers []models.TorguardServer, err error) {
for _, server := range t.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,78 @@
package torguard
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 (t *Torguard) BuildConf(connection models.OpenVPNConnection,
username string, settings configuration.OpenVPN) (lines []string) {
if settings.Cipher == "" {
settings.Cipher = constants.AES256gcm
}
if settings.Auth == "" {
settings.Auth = constants.SHA256
}
const defaultMSSFix = 1450
if settings.MSSFix == 0 {
settings.MSSFix = defaultMSSFix
}
lines = []string{
"client",
"dev tun",
"nobind",
"persist-key",
"remote-cert-tls server",
"tls-exit",
// Torguard specific
"tun-mtu 1500",
"tun-mtu-extra 32",
"mssfix " + strconv.Itoa(int(settings.MSSFix)),
"reneg-sec 0",
"fast-io",
"key-direction 1",
"script-security 2",
"ncp-disable",
"compress",
"keepalive 5 30",
"sndbuf 393216",
"rcvbuf 393216",
// Added constant values
"auth-nocache",
"mute-replay-warnings",
"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,
connection.ProtoLine(),
connection.RemoteLine(),
"data-ciphers-fallback " + settings.Cipher,
"data-ciphers " + settings.Cipher,
"auth " + settings.Auth,
}
if !settings.Root {
lines = append(lines, "user "+username)
}
lines = append(lines, utils.WrapOpenvpnCA(
constants.TorguardCertificate)...)
lines = append(lines, utils.WrapOpenvpnTLSAuth(
constants.TorguardOpenvpnStaticKeyV1)...)
lines = append(lines, "")
return lines
}

View File

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

View File

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

View File

@@ -1,61 +0,0 @@
package provider
import (
"context"
"math/rand"
"strings"
"time"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/logging"
)
type timeNowFunc func() time.Time
func tryUntilSuccessful(ctx context.Context, logger logging.Logger, fn func() error) {
const retryPeriod = 10 * time.Second
for {
err := fn()
if err == nil {
break
}
logger.Error(err)
logger.Info("Trying again in %s", retryPeriod)
timer := time.NewTimer(retryPeriod)
select {
case <-timer.C:
case <-ctx.Done():
if !timer.Stop() {
<-timer.C
}
return
}
}
}
func pickRandomConnection(connections []models.OpenVPNConnection, source rand.Source) models.OpenVPNConnection {
return connections[rand.New(source).Intn(len(connections))] //nolint:gosec
}
func filterByPossibilities(value string, possibilities []string) (filtered bool) {
if len(possibilities) == 0 {
return false
}
for _, possibility := range possibilities {
if strings.EqualFold(value, possibility) {
return false
}
}
return true
}
func commaJoin(slice []string) string {
return strings.Join(slice, ",")
}
func tcpBoolToProtocol(tcp bool) (protocol string) {
if tcp {
return "tcp"
}
return "udp"
}

View File

@@ -0,0 +1,15 @@
package utils
import "strings"
func FilterByPossibilities(value string, possibilities []string) (filtered bool) {
if len(possibilities) == 0 {
return false
}
for _, possibility := range possibilities {
if strings.EqualFold(value, possibility) {
return false
}
}
return true
}

View File

@@ -0,0 +1,35 @@
package utils
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_FilterByPossibilities(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
value string
possibilities []string
filtered bool
}{
"no possibilities": {},
"value not in possibilities": {
value: "c",
possibilities: []string{"a", "b"},
filtered: true,
},
"value in possibilities": {
value: "c",
possibilities: []string{"a", "b", "c"},
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
filtered := FilterByPossibilities(testCase.value, testCase.possibilities)
assert.Equal(t, testCase.filtered, filtered)
})
}
}

View File

@@ -0,0 +1,119 @@
package utils
import (
"errors"
"fmt"
"strconv"
"strings"
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/constants"
)
func commaJoin(slice []string) string {
return strings.Join(slice, ", ")
}
var ErrNoServerFound = errors.New("no server found")
func NoServerFoundError(selection configuration.ServerSelection) (err error) {
var messageParts []string
protocol := constants.UDP
if selection.TCP {
protocol = constants.TCP
}
messageParts = append(messageParts, "protocol "+protocol)
if selection.Group != "" {
part := "group " + selection.Group
messageParts = append(messageParts, part)
}
switch len(selection.Countries) {
case 0:
case 1:
part := "country " + selection.Countries[0]
messageParts = append(messageParts, part)
default:
part := "countries " + commaJoin(selection.Countries)
messageParts = append(messageParts, part)
}
switch len(selection.Regions) {
case 0:
case 1:
part := "region " + selection.Regions[0]
messageParts = append(messageParts, part)
default:
part := "regions " + commaJoin(selection.Regions)
messageParts = append(messageParts, part)
}
switch len(selection.Cities) {
case 0:
case 1:
part := "city " + selection.Cities[0]
messageParts = append(messageParts, part)
default:
part := "cities " + commaJoin(selection.Cities)
messageParts = append(messageParts, part)
}
if selection.Owned {
messageParts = append(messageParts, "owned servers only")
}
switch len(selection.ISPs) {
case 0:
case 1:
part := "ISP " + selection.ISPs[0]
messageParts = append(messageParts, part)
default:
part := "ISPs " + commaJoin(selection.ISPs)
messageParts = append(messageParts, part)
}
switch len(selection.Hostnames) {
case 0:
case 1:
part := "hostname " + selection.Hostnames[0]
messageParts = append(messageParts, part)
default:
part := "hostnames " + commaJoin(selection.Hostnames)
messageParts = append(messageParts, part)
}
switch len(selection.Names) {
case 0:
case 1:
part := "name " + selection.Names[0]
messageParts = append(messageParts, part)
default:
part := "names " + commaJoin(selection.Names)
messageParts = append(messageParts, part)
}
switch len(selection.Numbers) {
case 0:
case 1:
part := "server number " + strconv.Itoa(int(selection.Numbers[0]))
messageParts = append(messageParts, part)
default:
serverNumbers := make([]string, len(selection.Numbers))
for i := range selection.Numbers {
serverNumbers[i] = strconv.Itoa(int(selection.Numbers[i]))
}
part := "server numbers " + commaJoin(serverNumbers)
messageParts = append(messageParts, part)
}
if selection.EncryptionPreset != "" {
part := "encryption preset " + selection.EncryptionPreset
messageParts = append(messageParts, part)
}
message := "for " + strings.Join(messageParts, "; ")
return fmt.Errorf("%w: %s", ErrNoServerFound, message)
}

View File

@@ -0,0 +1,71 @@
package utils
func WrapOpenvpnCA(certificate string) (lines []string) {
return []string{
"<ca>",
"-----BEGIN CERTIFICATE-----",
certificate,
"-----END CERTIFICATE-----",
"</ca>",
}
}
func WrapOpenvpnCert(clientCertificate string) (lines []string) {
return []string{
"<cert>",
"-----BEGIN CERTIFICATE-----",
clientCertificate,
"-----END CERTIFICATE-----",
"</cert>",
}
}
func WrapOpenvpnCRLVerify(x509CRL string) (lines []string) {
return []string{
"<crl-verify>",
"-----BEGIN X509 CRL-----",
x509CRL,
"-----END X509 CRL-----",
"</crl-verify>",
}
}
func WrapOpenvpnKey(clientKey string) (lines []string) {
return []string{
"<key>",
"-----BEGIN PRIVATE KEY-----",
clientKey,
"-----END PRIVATE KEY-----",
"</key>",
}
}
func WrapOpenvpnRSAKey(rsaPrivateKey string) (lines []string) {
return []string{
"<key>",
"-----BEGIN RSA PRIVATE KEY-----",
rsaPrivateKey,
"-----END RSA PRIVATE KEY-----",
"</key>",
}
}
func WrapOpenvpnTLSAuth(staticKeyV1 string) (lines []string) {
return []string{
"<tls-auth>",
"-----BEGIN OpenVPN Static key V1-----",
staticKeyV1,
"-----END OpenVPN Static key V1-----",
"</tls-auth>",
}
}
func WrapOpenvpnTLSCrypt(staticKeyV1 string) (lines []string) {
return []string{
"<tls-crypt>",
"-----BEGIN OpenVPN Static key V1-----",
staticKeyV1,
"-----END OpenVPN Static key V1-----",
"</tls-crypt>",
}
}

View File

@@ -0,0 +1,12 @@
package utils
import (
"math/rand"
"github.com/qdm12/gluetun/internal/models"
)
func PickRandomConnection(connections []models.OpenVPNConnection,
source rand.Source) models.OpenVPNConnection {
return connections[rand.New(source).Intn(len(connections))] //nolint:gosec
}

View File

@@ -0,0 +1,26 @@
package utils
import (
"math/rand"
"testing"
"github.com/qdm12/gluetun/internal/models"
"github.com/stretchr/testify/assert"
)
func Test_PickRandomConnection(t *testing.T) {
t.Parallel()
connections := []models.OpenVPNConnection{
{Port: 1}, {Port: 2}, {Port: 3}, {Port: 4},
}
source := rand.NewSource(0)
connection := PickRandomConnection(connections, source)
assert.Equal(t, models.OpenVPNConnection{Port: 3}, connection)
connection = PickRandomConnection(connections, source)
assert.Equal(t, models.OpenVPNConnection{Port: 3}, connection)
connection = PickRandomConnection(connections, source)
assert.Equal(t, models.OpenVPNConnection{Port: 2}, connection)
}

View File

@@ -0,0 +1,22 @@
package utils
import (
"errors"
"fmt"
"net"
"github.com/qdm12/gluetun/internal/models"
)
var ErrTargetIPNotFound = errors.New("target IP address not found")
func GetTargetIPConnection(connections []models.OpenVPNConnection,
targetIP net.IP) (connection models.OpenVPNConnection, err error) {
for _, connection := range connections {
if targetIP.Equal(connection.IP) {
return connection, nil
}
}
return connection, fmt.Errorf("%w: in %d filtered connections",
ErrTargetIPNotFound, len(connections))
}

View File

@@ -1,54 +0,0 @@
package provider
import (
"math/rand"
"testing"
"github.com/qdm12/gluetun/internal/models"
"github.com/stretchr/testify/assert"
)
func Test_pickRandomConnection(t *testing.T) {
t.Parallel()
connections := []models.OpenVPNConnection{
{Port: 1}, {Port: 2}, {Port: 3}, {Port: 4},
}
source := rand.NewSource(0)
connection := pickRandomConnection(connections, source)
assert.Equal(t, models.OpenVPNConnection{Port: 3}, connection)
connection = pickRandomConnection(connections, source)
assert.Equal(t, models.OpenVPNConnection{Port: 3}, connection)
connection = pickRandomConnection(connections, source)
assert.Equal(t, models.OpenVPNConnection{Port: 2}, connection)
}
func Test_filterByPossibilities(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
value string
possibilities []string
filtered bool
}{
"no possibilities": {},
"value not in possibilities": {
value: "c",
possibilities: []string{"a", "b"},
filtered: true,
},
"value in possibilities": {
value: "c",
possibilities: []string{"a", "b", "c"},
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
filtered := filterByPossibilities(testCase.value, testCase.possibilities)
assert.Equal(t, testCase.filtered, filtered)
})
}
}

View File

@@ -1,134 +0,0 @@
package provider
import (
"context"
"fmt"
"math/rand"
"net"
"net/http"
"strconv"
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
type vyprvpn struct {
servers []models.VyprvpnServer
randSource rand.Source
}
func newVyprvpn(servers []models.VyprvpnServer, timeNow timeNowFunc) *vyprvpn {
return &vyprvpn{
servers: servers,
randSource: rand.NewSource(timeNow().UnixNano()),
}
}
func (v *vyprvpn) filterServers(regions, hostnames []string, tcp bool) (servers []models.VyprvpnServer) {
for _, server := range v.servers {
switch {
case
filterByPossibilities(server.Region, regions),
filterByPossibilities(server.Hostname, hostnames),
tcp && !server.TCP,
!tcp && !server.UDP:
default:
servers = append(servers, server)
}
}
return servers
}
func (v *vyprvpn) GetOpenVPNConnection(selection configuration.ServerSelection) (
connection models.OpenVPNConnection, err error) {
var port uint16
const protocol = constants.TCP
if selection.TCP {
return connection, fmt.Errorf("%w: TCP for provider VyprVPN",
ErrProtocolUnsupported)
}
if selection.TargetIP != nil {
return models.OpenVPNConnection{IP: selection.TargetIP, Port: port, Protocol: protocol}, nil
}
servers := v.filterServers(selection.Regions, selection.Hostnames, selection.TCP)
if len(servers) == 0 {
return connection, fmt.Errorf("no server found for region %s", commaJoin(selection.Regions))
}
var connections []models.OpenVPNConnection
for _, server := range servers {
for _, IP := range server.IPs {
connections = append(connections, models.OpenVPNConnection{IP: IP, Port: port, Protocol: protocol})
}
}
return pickRandomConnection(connections, v.randSource), nil
}
func (v *vyprvpn) BuildConf(connection models.OpenVPNConnection,
username string, settings configuration.OpenVPN) (lines []string) {
if len(settings.Cipher) == 0 {
settings.Cipher = aes256cbc
}
if len(settings.Auth) == 0 {
settings.Auth = "SHA256"
}
lines = []string{
"client",
"dev tun",
"nobind",
"persist-key",
"remote-cert-tls server",
"ping 10",
"ping-exit 60",
"ping-timer-rem",
"tls-exit",
// Vyprvpn specific
"comp-lzo",
// "verify-x509-name lu1.vyprvpn.com name",
"tls-cipher TLS-ECDHE-RSA-WITH-AES-256-GCM-SHA384:TLS-DHE-RSA-WITH-AES-256-CBC-SHA256:TLS-DHE-RSA-WITH-AES-256-CBC-SHA", //nolint:lll
// Added constant values
"auth-nocache",
"mute-replay-warnings",
"pull-filter ignore \"auth-token\"", // prevent auth failed loops
"auth-retry nointeract",
"suppress-timestamps",
// Modified variables
fmt.Sprintf("verb %d", settings.Verbosity),
fmt.Sprintf("auth-user-pass %s", constants.OpenVPNAuthConf),
fmt.Sprintf("proto %s", connection.Protocol),
fmt.Sprintf("remote %s %d", connection.IP, connection.Port),
"data-ciphers-fallback " + settings.Cipher,
"data-ciphers " + settings.Cipher,
fmt.Sprintf("auth %s", settings.Auth),
}
if !settings.Root {
lines = append(lines, "user "+username)
}
if settings.MSSFix > 0 {
lines = append(lines, "mssfix "+strconv.Itoa(int(settings.MSSFix)))
}
lines = append(lines, []string{
"<ca>",
"-----BEGIN CERTIFICATE-----",
constants.VyprvpnCertificate,
"-----END CERTIFICATE-----",
"</ca>",
}...)
return lines
}
func (v *vyprvpn) 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 vyprvpn")
}

View File

@@ -0,0 +1,45 @@
package vyprvpn
import (
"errors"
"fmt"
"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 (v *Vyprvpn) GetOpenVPNConnection(selection configuration.ServerSelection) (
connection models.OpenVPNConnection, err error) {
const port = 443
const protocol = constants.UDP
if selection.TCP {
return connection, fmt.Errorf("%w: TCP for provider VyprVPN", ErrProtocolUnsupported)
}
servers, err := v.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,
}
connections = append(connections, connection)
}
}
if selection.TargetIP != nil {
return utils.GetTargetIPConnection(connections, selection.TargetIP)
}
return utils.PickRandomConnection(connections, v.randSource), nil
}

View File

@@ -0,0 +1,28 @@
package vyprvpn
import (
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/provider/utils"
)
func (v *Vyprvpn) filterServers(selection configuration.ServerSelection) (
servers []models.VyprvpnServer, err error) {
for _, server := range v.servers {
switch {
case
utils.FilterByPossibilities(server.Region, selection.Regions),
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,69 @@
package vyprvpn
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 (v *Vyprvpn) 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",
"remote-cert-tls server",
"ping 10",
"ping-exit 60",
"ping-timer-rem",
"tls-exit",
// Vyprvpn specific
"comp-lzo",
// "verify-x509-name lu1.vyprvpn.com name",
"tls-cipher TLS-ECDHE-RSA-WITH-AES-256-GCM-SHA384:TLS-DHE-RSA-WITH-AES-256-CBC-SHA256:TLS-DHE-RSA-WITH-AES-256-CBC-SHA", //nolint:lll
// Added constant values
"auth-nocache",
"mute-replay-warnings",
"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,
connection.ProtoLine(),
connection.RemoteLine(),
"data-ciphers-fallback " + settings.Cipher,
"data-ciphers " + settings.Cipher,
"auth " + settings.Auth,
}
if !settings.Root {
lines = append(lines, "user "+username)
}
if settings.MSSFix > 0 {
lines = append(lines, "mssfix "+strconv.Itoa(int(settings.MSSFix)))
}
lines = append(lines, utils.WrapOpenvpnCA(
constants.VyprvpnCertificate)...)
lines = append(lines, "")
return lines
}

View File

@@ -0,0 +1,17 @@
package vyprvpn
import (
"context"
"net"
"net/http"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
func (v *Vyprvpn) PortForward(ctx context.Context, clienv *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 Vyprvpn")
}

View File

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

View File

@@ -1,156 +0,0 @@
package provider
import (
"context"
"fmt"
"math/rand"
"net"
"net/http"
"strconv"
"strings"
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/os"
)
type windscribe struct {
servers []models.WindscribeServer
randSource rand.Source
}
func newWindscribe(servers []models.WindscribeServer, timeNow timeNowFunc) *windscribe {
return &windscribe{
servers: servers,
randSource: rand.NewSource(timeNow().UnixNano()),
}
}
func (w *windscribe) filterServers(regions, cities, hostnames []string) (servers []models.WindscribeServer) {
for _, server := range w.servers {
switch {
case
filterByPossibilities(server.Region, regions),
filterByPossibilities(server.City, cities),
filterByPossibilities(server.Hostname, hostnames):
default:
servers = append(servers, server)
}
}
return servers
}
//nolint:lll
func (w *windscribe) GetOpenVPNConnection(selection configuration.ServerSelection) (connection models.OpenVPNConnection, err error) {
var port uint16 = 443
protocol := constants.UDP
if selection.TCP {
port = 1194
protocol = constants.TCP
}
if selection.CustomPort > 0 {
port = selection.CustomPort
}
if selection.TargetIP != nil {
return models.OpenVPNConnection{IP: selection.TargetIP, Port: port, Protocol: protocol}, nil
}
servers := w.filterServers(selection.Regions, selection.Cities, selection.Hostnames)
if len(servers) == 0 {
return connection, fmt.Errorf("no server found for region %s", commaJoin(selection.Regions))
}
var connections []models.OpenVPNConnection
for _, server := range servers {
for _, ip := range server.IPs {
connection := models.OpenVPNConnection{
IP: ip,
Port: port,
Protocol: protocol,
}
connections = append(connections, connection)
}
}
return pickRandomConnection(connections, w.randSource), nil
}
func (w *windscribe) BuildConf(connection models.OpenVPNConnection,
username string, settings configuration.OpenVPN) (lines []string) {
if len(settings.Cipher) == 0 {
settings.Cipher = aes256cbc
}
if len(settings.Auth) == 0 {
settings.Auth = "sha512"
}
lines = []string{
"client",
"dev tun",
"nobind",
"persist-key",
"remote-cert-tls server",
"ping 10",
"ping-exit 60",
"ping-timer-rem",
"tls-exit",
// Windscribe specific
"comp-lzo",
"key-direction 1",
"script-security 2",
"reneg-sec 0",
"ncp-disable",
// Added constant values
"auth-nocache",
"mute-replay-warnings",
"pull-filter ignore \"auth-token\"", // prevent auth failed loops
"auth-retry nointeract",
"suppress-timestamps",
// Modified variables
fmt.Sprintf("verb %d", settings.Verbosity),
fmt.Sprintf("auth-user-pass %s", constants.OpenVPNAuthConf),
fmt.Sprintf("proto %s", connection.Protocol),
fmt.Sprintf("remote %s %d", connection.IP, connection.Port),
"data-ciphers-fallback " + settings.Cipher,
"data-ciphers " + settings.Cipher,
fmt.Sprintf("auth %s", settings.Auth),
}
if strings.HasSuffix(settings.Cipher, "-gcm") {
lines = append(lines, "ncp-ciphers AES-256-GCM:AES-256-CBC:AES-128-GCM")
}
if !settings.Root {
lines = append(lines, "user "+username)
}
if settings.MSSFix > 0 {
lines = append(lines, "mssfix "+strconv.Itoa(int(settings.MSSFix)))
}
lines = append(lines, []string{
"<ca>",
"-----BEGIN CERTIFICATE-----",
constants.WindscribeCertificate,
"-----END CERTIFICATE-----",
"</ca>",
}...)
lines = append(lines, []string{
"<tls-auth>",
"-----BEGIN OpenVPN Static key V1-----",
constants.WindscribeOpenvpnStaticKeyV1,
"-----END OpenVPN Static key V1-----",
"</tls-auth>",
"",
}...)
return lines
}
func (w *windscribe) 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 windscribe")
}

View File

@@ -0,0 +1,45 @@
package windscribe
import (
"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 (w *Windscribe) GetOpenVPNConnection(selection configuration.ServerSelection) (
connection models.OpenVPNConnection, err error) {
protocol := constants.UDP
var port uint16 = 443
if selection.TCP {
protocol = constants.TCP
port = 1194
}
if selection.CustomPort > 0 {
port = selection.CustomPort
}
servers, err := w.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,
}
connections = append(connections, connection)
}
}
if selection.TargetIP != nil {
return utils.GetTargetIPConnection(connections, selection.TargetIP)
}
return utils.PickRandomConnection(connections, w.randSource), nil
}

Some files were not shown because too many files have changed in this diff Show More