diff --git a/internal/constants/openvpn.go b/internal/constants/openvpn.go
index 3853cfa4..a6fd3471 100644
--- a/internal/constants/openvpn.go
+++ b/internal/constants/openvpn.go
@@ -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"
+)
diff --git a/internal/models/openvpn.go b/internal/models/openvpn.go
index c776678a..e579ee4c 100644
--- a/internal/models/openvpn.go
+++ b/internal/models/openvpn.go
@@ -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
+}
diff --git a/internal/provider/constants.go b/internal/provider/constants.go
deleted file mode 100644
index 123b76cd..00000000
--- a/internal/provider/constants.go
+++ /dev/null
@@ -1,8 +0,0 @@
-package provider
-
-const (
- aes256cbc = "aes-256-cbc"
- aes128gcm = "aes-128-gcm"
- aes256gcm = "aes-256-gcm"
- sha256 = "sha256"
-)
diff --git a/internal/provider/cyberghost.go b/internal/provider/cyberghost.go
deleted file mode 100644
index 62a61591..00000000
--- a/internal/provider/cyberghost.go
+++ /dev/null
@@ -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{
- "",
- "-----BEGIN CERTIFICATE-----",
- constants.CyberghostCertificate,
- "-----END CERTIFICATE-----",
- "",
- }...)
- lines = append(lines, []string{
- "",
- "-----BEGIN CERTIFICATE-----",
- settings.Provider.ExtraConfigOptions.ClientCertificate,
- "-----END CERTIFICATE-----",
- "",
- }...)
- lines = append(lines, []string{
- "",
- "-----BEGIN PRIVATE KEY-----",
- settings.Provider.ExtraConfigOptions.ClientKey,
- "-----END PRIVATE 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")
-}
diff --git a/internal/provider/cyberghost/connection.go b/internal/provider/cyberghost/connection.go
new file mode 100644
index 00000000..8b130a48
--- /dev/null
+++ b/internal/provider/cyberghost/connection.go
@@ -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
+}
diff --git a/internal/provider/cyberghost/filter.go b/internal/provider/cyberghost/filter.go
new file mode 100644
index 00000000..7f51823b
--- /dev/null
+++ b/internal/provider/cyberghost/filter.go
@@ -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
+}
diff --git a/internal/provider/cyberghost_test.go b/internal/provider/cyberghost/filter_test.go
similarity index 68%
rename from internal/provider/cyberghost_test.go
rename to internal/provider/cyberghost/filter_test.go
index 0a155791..ae1ba355 100644
--- a/internal/provider/cyberghost_test.go
+++ b/internal/provider/cyberghost/filter_test.go
@@ -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)
})
}
diff --git a/internal/provider/cyberghost/openvpnconf.go b/internal/provider/cyberghost/openvpnconf.go
new file mode 100644
index 00000000..e4bdbf8f
--- /dev/null
+++ b/internal/provider/cyberghost/openvpnconf.go
@@ -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
+}
diff --git a/internal/provider/cyberghost/portforward.go b/internal/provider/cyberghost/portforward.go
new file mode 100644
index 00000000..5ec4c4ba
--- /dev/null
+++ b/internal/provider/cyberghost/portforward.go
@@ -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")
+}
diff --git a/internal/provider/cyberghost/provider.go b/internal/provider/cyberghost/provider.go
new file mode 100644
index 00000000..0d7325f1
--- /dev/null
+++ b/internal/provider/cyberghost/provider.go
@@ -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,
+ }
+}
diff --git a/internal/provider/errors.go b/internal/provider/errors.go
deleted file mode 100644
index dc24db52..00000000
--- a/internal/provider/errors.go
+++ /dev/null
@@ -1,5 +0,0 @@
-package provider
-
-import "errors"
-
-var ErrNoServerFound = errors.New("no server found")
diff --git a/internal/provider/fastestvpn.go b/internal/provider/fastestvpn.go
deleted file mode 100644
index 26876389..00000000
--- a/internal/provider/fastestvpn.go
+++ /dev/null
@@ -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{
- "",
- "-----BEGIN CERTIFICATE-----",
- constants.FastestvpnCertificate,
- "-----END CERTIFICATE-----",
- "",
- }...)
-
- lines = append(lines, []string{
- "",
- "-----BEGIN OpenVPN Static key V1-----",
- constants.FastestvpnOpenvpnStaticKeyV1,
- "-----END OpenVPN Static key V1-----",
- "",
- "",
- }...)
-
- 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")
-}
diff --git a/internal/provider/fastestvpn/connection.go b/internal/provider/fastestvpn/connection.go
new file mode 100644
index 00000000..15d77c70
--- /dev/null
+++ b/internal/provider/fastestvpn/connection.go
@@ -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
+}
diff --git a/internal/provider/fastestvpn/filter.go b/internal/provider/fastestvpn/filter.go
new file mode 100644
index 00000000..dea2fb85
--- /dev/null
+++ b/internal/provider/fastestvpn/filter.go
@@ -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
+}
diff --git a/internal/provider/fastestvpn/openvpnconf.go b/internal/provider/fastestvpn/openvpnconf.go
new file mode 100644
index 00000000..5e9ea719
--- /dev/null
+++ b/internal/provider/fastestvpn/openvpnconf.go
@@ -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
+}
diff --git a/internal/provider/fastestvpn/portforward.go b/internal/provider/fastestvpn/portforward.go
new file mode 100644
index 00000000..c4d6dddf
--- /dev/null
+++ b/internal/provider/fastestvpn/portforward.go
@@ -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")
+}
diff --git a/internal/provider/fastestvpn/provider.go b/internal/provider/fastestvpn/provider.go
new file mode 100644
index 00000000..7cce6716
--- /dev/null
+++ b/internal/provider/fastestvpn/provider.go
@@ -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,
+ }
+}
diff --git a/internal/provider/hidemyass.go b/internal/provider/hidemyass.go
deleted file mode 100644
index 8f2daa9e..00000000
--- a/internal/provider/hidemyass.go
+++ /dev/null
@@ -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{
- "",
- "-----BEGIN CERTIFICATE-----",
- constants.HideMyAssCA,
- "-----END CERTIFICATE-----",
- "",
- "",
- "-----BEGIN CERTIFICATE-----",
- constants.HideMyAssCertificate,
- "-----END CERTIFICATE-----",
- "",
- "",
- "-----BEGIN RSA PRIVATE KEY-----",
- constants.HideMyAssRSAPrivateKey,
- "-----END RSA PRIVATE 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")
-}
diff --git a/internal/provider/hidemyass/connection.go b/internal/provider/hidemyass/connection.go
new file mode 100644
index 00000000..3582ba42
--- /dev/null
+++ b/internal/provider/hidemyass/connection.go
@@ -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
+}
diff --git a/internal/provider/hidemyass/filter.go b/internal/provider/hidemyass/filter.go
new file mode 100644
index 00000000..85911238
--- /dev/null
+++ b/internal/provider/hidemyass/filter.go
@@ -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
+}
diff --git a/internal/provider/hidemyass/openvpnconf.go b/internal/provider/hidemyass/openvpnconf.go
new file mode 100644
index 00000000..e7b7ffcd
--- /dev/null
+++ b/internal/provider/hidemyass/openvpnconf.go
@@ -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
+}
diff --git a/internal/provider/hidemyass/portforward.go b/internal/provider/hidemyass/portforward.go
new file mode 100644
index 00000000..d4b8c3f1
--- /dev/null
+++ b/internal/provider/hidemyass/portforward.go
@@ -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")
+}
diff --git a/internal/provider/hidemyass/provider.go b/internal/provider/hidemyass/provider.go
new file mode 100644
index 00000000..73e33ccc
--- /dev/null
+++ b/internal/provider/hidemyass/provider.go
@@ -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,
+ }
+}
diff --git a/internal/provider/mullvad.go b/internal/provider/mullvad.go
deleted file mode 100644
index 9458ebdb..00000000
--- a/internal/provider/mullvad.go
+++ /dev/null
@@ -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{
- "",
- "-----BEGIN CERTIFICATE-----",
- constants.MullvadCertificate,
- "-----END CERTIFICATE-----",
- "",
- "",
- }...)
- 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")
-}
diff --git a/internal/provider/mullvad/connection.go b/internal/provider/mullvad/connection.go
new file mode 100644
index 00000000..0eb7c8b1
--- /dev/null
+++ b/internal/provider/mullvad/connection.go
@@ -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
+}
diff --git a/internal/provider/mullvad/filter.go b/internal/provider/mullvad/filter.go
new file mode 100644
index 00000000..8bc30be2
--- /dev/null
+++ b/internal/provider/mullvad/filter.go
@@ -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
+}
diff --git a/internal/provider/mullvad/openvpnconf.go b/internal/provider/mullvad/openvpnconf.go
new file mode 100644
index 00000000..58087509
--- /dev/null
+++ b/internal/provider/mullvad/openvpnconf.go
@@ -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
+}
diff --git a/internal/provider/mullvad/portforward.go b/internal/provider/mullvad/portforward.go
new file mode 100644
index 00000000..ebe3ce4f
--- /dev/null
+++ b/internal/provider/mullvad/portforward.go
@@ -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")
+}
diff --git a/internal/provider/mullvad/provider.go b/internal/provider/mullvad/provider.go
new file mode 100644
index 00000000..12d1675d
--- /dev/null
+++ b/internal/provider/mullvad/provider.go
@@ -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,
+ }
+}
diff --git a/internal/provider/nordvpn.go b/internal/provider/nordvpn.go
deleted file mode 100644
index e4780e10..00000000
--- a/internal/provider/nordvpn.go
+++ /dev/null
@@ -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{
- "",
- "-----BEGIN CERTIFICATE-----",
- constants.NordvpnCertificate,
- "-----END CERTIFICATE-----",
- "",
- }...)
- lines = append(lines, []string{
- "",
- "-----BEGIN OpenVPN Static key V1-----",
- constants.NordvpnOpenvpnStaticKeyV1,
- "-----END OpenVPN Static key V1-----",
- "",
- "",
- }...)
- 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")
-}
diff --git a/internal/provider/nordvpn/connection.go b/internal/provider/nordvpn/connection.go
new file mode 100644
index 00000000..5f483db7
--- /dev/null
+++ b/internal/provider/nordvpn/connection.go
@@ -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
+}
diff --git a/internal/provider/nordvpn/filter.go b/internal/provider/nordvpn/filter.go
new file mode 100644
index 00000000..7b49a299
--- /dev/null
+++ b/internal/provider/nordvpn/filter.go
@@ -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
+}
diff --git a/internal/provider/nordvpn/openvpnconf.go b/internal/provider/nordvpn/openvpnconf.go
new file mode 100644
index 00000000..0b0aea90
--- /dev/null
+++ b/internal/provider/nordvpn/openvpnconf.go
@@ -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
+}
diff --git a/internal/provider/nordvpn/portforward.go b/internal/provider/nordvpn/portforward.go
new file mode 100644
index 00000000..df46f828
--- /dev/null
+++ b/internal/provider/nordvpn/portforward.go
@@ -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")
+}
diff --git a/internal/provider/nordvpn/provider.go b/internal/provider/nordvpn/provider.go
new file mode 100644
index 00000000..7e39f7f3
--- /dev/null
+++ b/internal/provider/nordvpn/provider.go
@@ -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,
+ }
+}
diff --git a/internal/provider/piav4.go b/internal/provider/piav4.go
deleted file mode 100644
index 9be2ce4a..00000000
--- a/internal/provider/piav4.go
+++ /dev/null
@@ -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{
- "",
- "-----BEGIN X509 CRL-----",
- X509CRL,
- "-----END X509 CRL-----",
- "",
- }...)
- lines = append(lines, []string{
- "",
- "-----BEGIN CERTIFICATE-----",
- certificate,
- "-----END CERTIFICATE-----",
- "",
- "",
- }...)
- 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: "", password: ""})
- }
- response, err := client.Do(request)
- if err != nil {
- return "", replaceInErr(err, map[string]string{
- username: "", 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: ""})
- 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: ""})
- 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: "",
- data.Signature: "",
- })
- }
- response, err := client.Do(request)
- if err != nil {
- return replaceInErr(err, map[string]string{
- payload: "",
- data.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)
-}
diff --git a/internal/provider/privado.go b/internal/provider/privado.go
deleted file mode 100644
index 95127034..00000000
--- a/internal/provider/privado.go
+++ /dev/null
@@ -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{
- "",
- "-----BEGIN CERTIFICATE-----",
- constants.PrivadoCertificate,
- "-----END CERTIFICATE-----",
- "",
- }...)
- 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")
-}
diff --git a/internal/provider/privado/connection.go b/internal/provider/privado/connection.go
new file mode 100644
index 00000000..a512c5f2
--- /dev/null
+++ b/internal/provider/privado/connection.go
@@ -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
+}
diff --git a/internal/provider/privado/filter.go b/internal/provider/privado/filter.go
new file mode 100644
index 00000000..3cdf79c9
--- /dev/null
+++ b/internal/provider/privado/filter.go
@@ -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
+}
diff --git a/internal/provider/privado/openvpnconf.go b/internal/provider/privado/openvpnconf.go
new file mode 100644
index 00000000..a19d1697
--- /dev/null
+++ b/internal/provider/privado/openvpnconf.go
@@ -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
+}
diff --git a/internal/provider/privado/portforward.go b/internal/provider/privado/portforward.go
new file mode 100644
index 00000000..3ad0065c
--- /dev/null
+++ b/internal/provider/privado/portforward.go
@@ -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")
+}
diff --git a/internal/provider/privado/provider.go b/internal/provider/privado/provider.go
new file mode 100644
index 00000000..5616926d
--- /dev/null
+++ b/internal/provider/privado/provider.go
@@ -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,
+ }
+}
diff --git a/internal/provider/privateinternetaccess/connection.go b/internal/provider/privateinternetaccess/connection.go
new file mode 100644
index 00000000..1c770b6e
--- /dev/null
+++ b/internal/provider/privateinternetaccess/connection.go
@@ -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
+}
diff --git a/internal/provider/privateinternetaccess/filter.go b/internal/provider/privateinternetaccess/filter.go
new file mode 100644
index 00000000..1f1b3501
--- /dev/null
+++ b/internal/provider/privateinternetaccess/filter.go
@@ -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
+}
diff --git a/internal/provider/privateinternetaccess/httpclient.go b/internal/provider/privateinternetaccess/httpclient.go
new file mode 100644
index 00000000..c3e80502
--- /dev/null
+++ b/internal/provider/privateinternetaccess/httpclient.go
@@ -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
+}
diff --git a/internal/provider/privateinternetaccess/openvpnconf.go b/internal/provider/privateinternetaccess/openvpnconf.go
new file mode 100644
index 00000000..41727757
--- /dev/null
+++ b/internal/provider/privateinternetaccess/openvpnconf.go
@@ -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
+}
diff --git a/internal/provider/privateinternetaccess/port.go b/internal/provider/privateinternetaccess/port.go
new file mode 100644
index 00000000..cfdf2979
--- /dev/null
+++ b/internal/provider/privateinternetaccess/port.go
@@ -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)
+ }
+}
diff --git a/internal/provider/privateinternetaccess/portforward.go b/internal/provider/privateinternetaccess/portforward.go
new file mode 100644
index 00000000..3eef1e86
--- /dev/null
+++ b/internal/provider/privateinternetaccess/portforward.go
@@ -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: "",
+ 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: ""}
+
+ 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: "",
+ data.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)
+}
diff --git a/internal/provider/piav4_test.go b/internal/provider/privateinternetaccess/portforward_test.go
similarity index 70%
rename from internal/provider/piav4_test.go
rename to internal/provider/privateinternetaccess/portforward_test.go
index b03284b3..4b4902f7 100644
--- a/internal/provider/piav4_test.go
+++ b/internal/provider/privateinternetaccess/portforward_test.go
@@ -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)
+ })
+ }
+}
diff --git a/internal/provider/privateinternetaccess/provider.go b/internal/provider/privateinternetaccess/provider.go
new file mode 100644
index 00000000..856dffe9
--- /dev/null
+++ b/internal/provider/privateinternetaccess/provider.go
@@ -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,
+ }
+}
diff --git a/internal/provider/privateinternetaccess/try.go b/internal/provider/privateinternetaccess/try.go
new file mode 100644
index 00000000..0be4a55a
--- /dev/null
+++ b/internal/provider/privateinternetaccess/try.go
@@ -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
+ }
+}
diff --git a/internal/provider/privatevpn.go b/internal/provider/privatevpn.go
deleted file mode 100644
index d098eb78..00000000
--- a/internal/provider/privatevpn.go
+++ /dev/null
@@ -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{
- "",
- "-----BEGIN CERTIFICATE-----",
- constants.PrivatevpnCertificate,
- "-----END CERTIFICATE-----",
- "",
- }...)
- lines = append(lines, []string{
- "",
- "-----BEGIN OpenVPN Static key V1-----",
- constants.PrivatevpnOpenvpnStaticKeyV1,
- "-----END OpenVPN Static key V1-----",
- "",
- "",
- }...)
- 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")
-}
diff --git a/internal/provider/privatevpn/connection.go b/internal/provider/privatevpn/connection.go
new file mode 100644
index 00000000..97652359
--- /dev/null
+++ b/internal/provider/privatevpn/connection.go
@@ -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
+}
diff --git a/internal/provider/privatevpn/filter.go b/internal/provider/privatevpn/filter.go
new file mode 100644
index 00000000..af2b9c5b
--- /dev/null
+++ b/internal/provider/privatevpn/filter.go
@@ -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
+}
diff --git a/internal/provider/privatevpn/openvpnconf.go b/internal/provider/privatevpn/openvpnconf.go
new file mode 100644
index 00000000..4bd23ba0
--- /dev/null
+++ b/internal/provider/privatevpn/openvpnconf.go
@@ -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
+}
diff --git a/internal/provider/privatevpn/portforward.go b/internal/provider/privatevpn/portforward.go
new file mode 100644
index 00000000..691ad37b
--- /dev/null
+++ b/internal/provider/privatevpn/portforward.go
@@ -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")
+}
diff --git a/internal/provider/privatevpn/provider.go b/internal/provider/privatevpn/provider.go
new file mode 100644
index 00000000..5782d5c8
--- /dev/null
+++ b/internal/provider/privatevpn/provider.go
@@ -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,
+ }
+}
diff --git a/internal/provider/protonvpn.go b/internal/provider/protonvpn.go
deleted file mode 100644
index 7843826d..00000000
--- a/internal/provider/protonvpn.go
+++ /dev/null
@@ -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{
- "",
- "-----BEGIN CERTIFICATE-----",
- constants.ProtonvpnCertificate,
- "-----END CERTIFICATE-----",
- "",
- }...)
- lines = append(lines, []string{
- "",
- "-----BEGIN OpenVPN Static key V1-----",
- constants.ProtonvpnOpenvpnStaticKeyV1,
- "-----END OpenVPN Static key V1-----",
- "",
- "",
- }...)
- 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)
-}
diff --git a/internal/provider/protonvpn/connection.go b/internal/provider/protonvpn/connection.go
new file mode 100644
index 00000000..37f86dac
--- /dev/null
+++ b/internal/provider/protonvpn/connection.go
@@ -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
+}
diff --git a/internal/provider/protonvpn/filter.go b/internal/provider/protonvpn/filter.go
new file mode 100644
index 00000000..143c2c40
--- /dev/null
+++ b/internal/provider/protonvpn/filter.go
@@ -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
+}
diff --git a/internal/provider/protonvpn/openvpnconf.go b/internal/provider/protonvpn/openvpnconf.go
new file mode 100644
index 00000000..5774abcf
--- /dev/null
+++ b/internal/provider/protonvpn/openvpnconf.go
@@ -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
+}
diff --git a/internal/provider/protonvpn/port.go b/internal/provider/protonvpn/port.go
new file mode 100644
index 00000000..b6903cdd
--- /dev/null
+++ b/internal/provider/protonvpn/port.go
@@ -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)
+ }
+}
diff --git a/internal/provider/protonvpn/portforward.go b/internal/provider/protonvpn/portforward.go
new file mode 100644
index 00000000..fd86af10
--- /dev/null
+++ b/internal/provider/protonvpn/portforward.go
@@ -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")
+}
diff --git a/internal/provider/protonvpn/provider.go b/internal/provider/protonvpn/provider.go
new file mode 100644
index 00000000..d0521361
--- /dev/null
+++ b/internal/provider/protonvpn/provider.go
@@ -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,
+ }
+}
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index b1a20319..a3c79213 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -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
}
diff --git a/internal/provider/purevpn.go b/internal/provider/purevpn.go
deleted file mode 100644
index 434586ff..00000000
--- a/internal/provider/purevpn.go
+++ /dev/null
@@ -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{
- "",
- "-----BEGIN CERTIFICATE-----",
- constants.PurevpnCertificateAuthority,
- "-----END CERTIFICATE-----",
- "",
- }...)
- lines = append(lines, []string{
- "",
- "-----BEGIN CERTIFICATE-----",
- constants.PurevpnCertificate,
- "-----END CERTIFICATE-----",
- "",
- }...)
- lines = append(lines, []string{
- "",
- "-----BEGIN PRIVATE KEY-----",
- constants.PurevpnKey,
- "-----END PRIVATE KEY-----",
- "",
- "",
- }...)
- lines = append(lines, []string{
- "",
- "-----BEGIN OpenVPN Static key V1-----",
- constants.PurevpnOpenvpnStaticKeyV1,
- "-----END OpenVPN Static key V1-----",
- "",
- "",
- }...)
- 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")
-}
diff --git a/internal/provider/purevpn/connection.go b/internal/provider/purevpn/connection.go
new file mode 100644
index 00000000..e1ad5e43
--- /dev/null
+++ b/internal/provider/purevpn/connection.go
@@ -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
+}
diff --git a/internal/provider/purevpn/filter.go b/internal/provider/purevpn/filter.go
new file mode 100644
index 00000000..30c481ba
--- /dev/null
+++ b/internal/provider/purevpn/filter.go
@@ -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
+}
diff --git a/internal/provider/purevpn/openvpnconf.go b/internal/provider/purevpn/openvpnconf.go
new file mode 100644
index 00000000..b3adec02
--- /dev/null
+++ b/internal/provider/purevpn/openvpnconf.go
@@ -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
+}
diff --git a/internal/provider/purevpn/portforward.go b/internal/provider/purevpn/portforward.go
new file mode 100644
index 00000000..5312681c
--- /dev/null
+++ b/internal/provider/purevpn/portforward.go
@@ -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")
+}
diff --git a/internal/provider/purevpn/provider.go b/internal/provider/purevpn/provider.go
new file mode 100644
index 00000000..07f0d99d
--- /dev/null
+++ b/internal/provider/purevpn/provider.go
@@ -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,
+ }
+}
diff --git a/internal/provider/surfshark.go b/internal/provider/surfshark.go
deleted file mode 100644
index 6b7af1fe..00000000
--- a/internal/provider/surfshark.go
+++ /dev/null
@@ -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{
- "",
- "-----BEGIN CERTIFICATE-----",
- constants.SurfsharkCertificate,
- "-----END CERTIFICATE-----",
- "",
- }...)
- lines = append(lines, []string{
- "",
- "-----BEGIN OpenVPN Static key V1-----",
- constants.SurfsharkOpenvpnStaticKeyV1,
- "-----END OpenVPN Static key V1-----",
- "",
- "",
- }...)
- 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")
-}
diff --git a/internal/provider/surfshark/connection.go b/internal/provider/surfshark/connection.go
new file mode 100644
index 00000000..23bf6a7c
--- /dev/null
+++ b/internal/provider/surfshark/connection.go
@@ -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
+}
diff --git a/internal/provider/surfshark/filter.go b/internal/provider/surfshark/filter.go
new file mode 100644
index 00000000..a80b93cb
--- /dev/null
+++ b/internal/provider/surfshark/filter.go
@@ -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
+}
diff --git a/internal/provider/surfshark/openvpnconf.go b/internal/provider/surfshark/openvpnconf.go
new file mode 100644
index 00000000..01acb27b
--- /dev/null
+++ b/internal/provider/surfshark/openvpnconf.go
@@ -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
+}
diff --git a/internal/provider/surfshark/portforward.go b/internal/provider/surfshark/portforward.go
new file mode 100644
index 00000000..5822a50b
--- /dev/null
+++ b/internal/provider/surfshark/portforward.go
@@ -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")
+}
diff --git a/internal/provider/surfshark/provider.go b/internal/provider/surfshark/provider.go
new file mode 100644
index 00000000..53fc6ce1
--- /dev/null
+++ b/internal/provider/surfshark/provider.go
@@ -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,
+ }
+}
diff --git a/internal/provider/torguard.go b/internal/provider/torguard.go
deleted file mode 100644
index dc527845..00000000
--- a/internal/provider/torguard.go
+++ /dev/null
@@ -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{
- "",
- "-----BEGIN CERTIFICATE-----",
- constants.TorguardCertificate,
- "-----END CERTIFICATE-----",
- "",
- }...)
-
- lines = append(lines, []string{
- "",
- "-----BEGIN OpenVPN Static key V1-----",
- constants.TorguardOpenvpnStaticKeyV1,
- "-----END OpenVPN Static key V1-----",
- "",
- "",
- }...)
-
- 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")
-}
diff --git a/internal/provider/torguard/connection.go b/internal/provider/torguard/connection.go
new file mode 100644
index 00000000..cf153578
--- /dev/null
+++ b/internal/provider/torguard/connection.go
@@ -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
+}
diff --git a/internal/provider/torguard/filter.go b/internal/provider/torguard/filter.go
new file mode 100644
index 00000000..d89ccaa4
--- /dev/null
+++ b/internal/provider/torguard/filter.go
@@ -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
+}
diff --git a/internal/provider/torguard/openvpnconf.go b/internal/provider/torguard/openvpnconf.go
new file mode 100644
index 00000000..71bf10a9
--- /dev/null
+++ b/internal/provider/torguard/openvpnconf.go
@@ -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
+}
diff --git a/internal/provider/torguard/portforward.go b/internal/provider/torguard/portforward.go
new file mode 100644
index 00000000..f7835a15
--- /dev/null
+++ b/internal/provider/torguard/portforward.go
@@ -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")
+}
diff --git a/internal/provider/torguard/provider.go b/internal/provider/torguard/provider.go
new file mode 100644
index 00000000..73406a5d
--- /dev/null
+++ b/internal/provider/torguard/provider.go
@@ -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,
+ }
+}
diff --git a/internal/provider/utils.go b/internal/provider/utils.go
deleted file mode 100644
index df80f0e4..00000000
--- a/internal/provider/utils.go
+++ /dev/null
@@ -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"
-}
diff --git a/internal/provider/utils/filtering.go b/internal/provider/utils/filtering.go
new file mode 100644
index 00000000..268536b2
--- /dev/null
+++ b/internal/provider/utils/filtering.go
@@ -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
+}
diff --git a/internal/provider/utils/filtering_test.go b/internal/provider/utils/filtering_test.go
new file mode 100644
index 00000000..9639e913
--- /dev/null
+++ b/internal/provider/utils/filtering_test.go
@@ -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)
+ })
+ }
+}
diff --git a/internal/provider/utils/formatting.go b/internal/provider/utils/formatting.go
new file mode 100644
index 00000000..c35a481d
--- /dev/null
+++ b/internal/provider/utils/formatting.go
@@ -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)
+}
diff --git a/internal/provider/utils/openvpn.go b/internal/provider/utils/openvpn.go
new file mode 100644
index 00000000..daf1b025
--- /dev/null
+++ b/internal/provider/utils/openvpn.go
@@ -0,0 +1,71 @@
+package utils
+
+func WrapOpenvpnCA(certificate string) (lines []string) {
+ return []string{
+ "",
+ "-----BEGIN CERTIFICATE-----",
+ certificate,
+ "-----END CERTIFICATE-----",
+ "",
+ }
+}
+
+func WrapOpenvpnCert(clientCertificate string) (lines []string) {
+ return []string{
+ "",
+ "-----BEGIN CERTIFICATE-----",
+ clientCertificate,
+ "-----END CERTIFICATE-----",
+ "",
+ }
+}
+
+func WrapOpenvpnCRLVerify(x509CRL string) (lines []string) {
+ return []string{
+ "",
+ "-----BEGIN X509 CRL-----",
+ x509CRL,
+ "-----END X509 CRL-----",
+ "",
+ }
+}
+
+func WrapOpenvpnKey(clientKey string) (lines []string) {
+ return []string{
+ "",
+ "-----BEGIN PRIVATE KEY-----",
+ clientKey,
+ "-----END PRIVATE KEY-----",
+ "",
+ }
+}
+
+func WrapOpenvpnRSAKey(rsaPrivateKey string) (lines []string) {
+ return []string{
+ "",
+ "-----BEGIN RSA PRIVATE KEY-----",
+ rsaPrivateKey,
+ "-----END RSA PRIVATE KEY-----",
+ "",
+ }
+}
+
+func WrapOpenvpnTLSAuth(staticKeyV1 string) (lines []string) {
+ return []string{
+ "",
+ "-----BEGIN OpenVPN Static key V1-----",
+ staticKeyV1,
+ "-----END OpenVPN Static key V1-----",
+ "",
+ }
+}
+
+func WrapOpenvpnTLSCrypt(staticKeyV1 string) (lines []string) {
+ return []string{
+ "",
+ "-----BEGIN OpenVPN Static key V1-----",
+ staticKeyV1,
+ "-----END OpenVPN Static key V1-----",
+ "",
+ }
+}
diff --git a/internal/provider/utils/pick.go b/internal/provider/utils/pick.go
new file mode 100644
index 00000000..a5a61456
--- /dev/null
+++ b/internal/provider/utils/pick.go
@@ -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
+}
diff --git a/internal/provider/utils/pick_test.go b/internal/provider/utils/pick_test.go
new file mode 100644
index 00000000..f5712379
--- /dev/null
+++ b/internal/provider/utils/pick_test.go
@@ -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)
+}
diff --git a/internal/provider/utils/targetip.go b/internal/provider/utils/targetip.go
new file mode 100644
index 00000000..3e01aea8
--- /dev/null
+++ b/internal/provider/utils/targetip.go
@@ -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))
+}
diff --git a/internal/provider/utils_test.go b/internal/provider/utils_test.go
deleted file mode 100644
index fcd6b2fa..00000000
--- a/internal/provider/utils_test.go
+++ /dev/null
@@ -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)
- })
- }
-}
diff --git a/internal/provider/vyprvpn.go b/internal/provider/vyprvpn.go
deleted file mode 100644
index a463fd72..00000000
--- a/internal/provider/vyprvpn.go
+++ /dev/null
@@ -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{
- "",
- "-----BEGIN CERTIFICATE-----",
- constants.VyprvpnCertificate,
- "-----END CERTIFICATE-----",
- "",
- }...)
- 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")
-}
diff --git a/internal/provider/vyprvpn/connection.go b/internal/provider/vyprvpn/connection.go
new file mode 100644
index 00000000..655c09bd
--- /dev/null
+++ b/internal/provider/vyprvpn/connection.go
@@ -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
+}
diff --git a/internal/provider/vyprvpn/filter.go b/internal/provider/vyprvpn/filter.go
new file mode 100644
index 00000000..cacc5dd7
--- /dev/null
+++ b/internal/provider/vyprvpn/filter.go
@@ -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
+}
diff --git a/internal/provider/vyprvpn/openvpnconf.go b/internal/provider/vyprvpn/openvpnconf.go
new file mode 100644
index 00000000..992c9c02
--- /dev/null
+++ b/internal/provider/vyprvpn/openvpnconf.go
@@ -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
+}
diff --git a/internal/provider/vyprvpn/portforward.go b/internal/provider/vyprvpn/portforward.go
new file mode 100644
index 00000000..9ede1359
--- /dev/null
+++ b/internal/provider/vyprvpn/portforward.go
@@ -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")
+}
diff --git a/internal/provider/vyprvpn/provider.go b/internal/provider/vyprvpn/provider.go
new file mode 100644
index 00000000..d84a1f1c
--- /dev/null
+++ b/internal/provider/vyprvpn/provider.go
@@ -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,
+ }
+}
diff --git a/internal/provider/windscribe.go b/internal/provider/windscribe.go
deleted file mode 100644
index 53e47352..00000000
--- a/internal/provider/windscribe.go
+++ /dev/null
@@ -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{
- "",
- "-----BEGIN CERTIFICATE-----",
- constants.WindscribeCertificate,
- "-----END CERTIFICATE-----",
- "",
- }...)
- lines = append(lines, []string{
- "",
- "-----BEGIN OpenVPN Static key V1-----",
- constants.WindscribeOpenvpnStaticKeyV1,
- "-----END OpenVPN Static key V1-----",
- "",
- "",
- }...)
- 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")
-}
diff --git a/internal/provider/windscribe/connection.go b/internal/provider/windscribe/connection.go
new file mode 100644
index 00000000..3460ce98
--- /dev/null
+++ b/internal/provider/windscribe/connection.go
@@ -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
+}
diff --git a/internal/provider/windscribe/filter.go b/internal/provider/windscribe/filter.go
new file mode 100644
index 00000000..d2bfc21b
--- /dev/null
+++ b/internal/provider/windscribe/filter.go
@@ -0,0 +1,27 @@
+package windscribe
+
+import (
+ "github.com/qdm12/gluetun/internal/configuration"
+ "github.com/qdm12/gluetun/internal/models"
+ "github.com/qdm12/gluetun/internal/provider/utils"
+)
+
+func (w *Windscribe) filterServers(selection configuration.ServerSelection) (
+ servers []models.WindscribeServer, err error) {
+ for _, server := range w.servers {
+ switch {
+ case
+ 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
+}
diff --git a/internal/provider/windscribe/openvpnconf.go b/internal/provider/windscribe/openvpnconf.go
new file mode 100644
index 00000000..38eadcb2
--- /dev/null
+++ b/internal/provider/windscribe/openvpnconf.go
@@ -0,0 +1,78 @@
+package windscribe
+
+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 (w *Windscribe) 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
+ }
+
+ 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
+ "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.WindscribeCertificate)...)
+ lines = append(lines, utils.WrapOpenvpnTLSAuth(
+ constants.WindscribeOpenvpnStaticKeyV1)...)
+
+ lines = append(lines, "")
+
+ return lines
+}
diff --git a/internal/provider/windscribe/portforward.go b/internal/provider/windscribe/portforward.go
new file mode 100644
index 00000000..eeccccb8
--- /dev/null
+++ b/internal/provider/windscribe/portforward.go
@@ -0,0 +1,17 @@
+package windscribe
+
+import (
+ "context"
+ "net"
+ "net/http"
+
+ "github.com/qdm12/gluetun/internal/firewall"
+ "github.com/qdm12/golibs/logging"
+ "github.com/qdm12/golibs/os"
+)
+
+func (w *Windscribe) PortForward(ctx context.Context, clienw *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")
+}
diff --git a/internal/provider/windscribe/provider.go b/internal/provider/windscribe/provider.go
new file mode 100644
index 00000000..6c716701
--- /dev/null
+++ b/internal/provider/windscribe/provider.go
@@ -0,0 +1,19 @@
+package windscribe
+
+import (
+ "math/rand"
+
+ "github.com/qdm12/gluetun/internal/models"
+)
+
+type Windscribe struct {
+ servers []models.WindscribeServer
+ randSource rand.Source
+}
+
+func New(servers []models.WindscribeServer, randSource rand.Source) *Windscribe {
+ return &Windscribe{
+ servers: servers,
+ randSource: randSource,
+ }
+}