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, + } +}