diff --git a/internal/cli/openvpnconfig.go b/internal/cli/openvpnconfig.go index 1ccdeaf5..9a489125 100644 --- a/internal/cli/openvpnconfig.go +++ b/internal/cli/openvpnconfig.go @@ -34,7 +34,11 @@ func (c *CLI) OpenvpnConfig(logger logging.Logger, env params.Interface) error { if err != nil { return err } - lines := providerConf.BuildConf(connection, allSettings.VPN.OpenVPN) + lines, err := providerConf.BuildConf(connection, allSettings.VPN.OpenVPN) + if err != nil { + return err + } + fmt.Println(strings.Join(lines, "\n")) return nil } diff --git a/internal/configuration/custom.go b/internal/configuration/custom.go new file mode 100644 index 00000000..544a18e0 --- /dev/null +++ b/internal/configuration/custom.go @@ -0,0 +1,53 @@ +package configuration + +import ( + "errors" + "fmt" + + "github.com/qdm12/gluetun/internal/constants" + "github.com/qdm12/golibs/params" +) + +var ( + errCustomNotSupported = errors.New("custom provider is not supported") + errCustomExtractFromFile = errors.New("cannot extract configuration from file") +) + +func (settings *Provider) readCustom(r reader, vpnType string) (err error) { + settings.Name = constants.Custom + + if vpnType != constants.OpenVPN { + return fmt.Errorf("%w: for VPN type %s", errCustomNotSupported, vpnType) + } + + return settings.readCustomOpenVPN(r) +} + +func (settings *Provider) readCustomOpenVPN(r reader) (err error) { + configFile, err := r.env.Get("OPENVPN_CUSTOM_CONFIG", params.CaseSensitiveValue(), params.Compulsory()) + if err != nil { + return fmt.Errorf("environment variable OPENVPN_CUSTOM_CONFIG: %w", err) + } + settings.ServerSelection.OpenVPN.ConfFile = configFile + + // For display and consistency purposes only, + // these values are not actually used since the file is re-read + // before each OpenVPN start. + _, connection, err := r.ovpnExt.Data(configFile) + if err != nil { + return fmt.Errorf("%w: %s", errCustomExtractFromFile, err) + } + settings.ServerSelection.OpenVPN.TCP = connection.Protocol == constants.TCP + + return nil +} + +func (settings *OpenVPN) readCustom(r reader) (err error) { + settings.ConfFile, err = r.env.Path("OPENVPN_CUSTOM_CONFIG", + params.Compulsory(), params.CaseSensitiveValue()) + if err != nil { + return fmt.Errorf("environment variable OPENVPN_CUSTOM_CONFIG: %w", err) + } + + return nil +} diff --git a/internal/configuration/openvpn.go b/internal/configuration/openvpn.go index cdcfafcd..4a7c4668 100644 --- a/internal/configuration/openvpn.go +++ b/internal/configuration/openvpn.go @@ -21,7 +21,7 @@ type OpenVPN struct { Root bool `json:"run_as_root"` Cipher string `json:"cipher"` Auth string `json:"auth"` - Config string `json:"custom_config"` + ConfFile string `json:"conf_file"` Version string `json:"version"` ClientCrt string `json:"-"` // Cyberghost ClientKey string `json:"-"` // Cyberghost, VPNUnlimited @@ -59,8 +59,8 @@ func (settings *OpenVPN) lines() (lines []string) { lines = append(lines, indent+lastIndent+"Custom auth algorithm: "+settings.Auth) } - if len(settings.Config) > 0 { - lines = append(lines, indent+lastIndent+"Custom configuration: "+settings.Config) + if settings.ConfFile != "" { + lines = append(lines, indent+lastIndent+"Configuration file: "+settings.ConfFile) } if settings.ClientKey != "" { @@ -83,13 +83,14 @@ func (settings *OpenVPN) lines() (lines []string) { } func (settings *OpenVPN) read(r reader, serviceProvider string) (err error) { - settings.Config, err = r.env.Get("OPENVPN_CUSTOM_CONFIG", params.CaseSensitiveValue()) - if err != nil { - return fmt.Errorf("environment variable OPENVPN_CUSTOM_CONFIG: %w", err) + credentialsRequired := false + switch serviceProvider { + case constants.Custom: + case constants.VPNUnlimited: + default: + credentialsRequired = true } - credentialsRequired := settings.Config == "" && serviceProvider != constants.VPNUnlimited - settings.User, err = r.getFromEnvOrSecretFile("OPENVPN_USER", credentialsRequired, []string{"USER"}) if err != nil { return fmt.Errorf("environment variable OPENVPN_USER: %w", err) @@ -159,6 +160,8 @@ func (settings *OpenVPN) read(r reader, serviceProvider string) (err error) { } switch serviceProvider { + case constants.Custom: + err = settings.readCustom(r) // read OPENVPN_CUSTOM_CONFIG case constants.Cyberghost: err = settings.readCyberghost(r) case constants.PrivateInternetAccess: diff --git a/internal/configuration/openvpn_test.go b/internal/configuration/openvpn_test.go index 2c58f488..cdbd775f 100644 --- a/internal/configuration/openvpn_test.go +++ b/internal/configuration/openvpn_test.go @@ -25,7 +25,7 @@ func Test_OpenVPN_JSON(t *testing.T) { "run_as_root": true, "cipher": "", "auth": "", - "custom_config": "", + "conf_file": "", "version": "", "encryption_preset": "", "ipv6": false, diff --git a/internal/configuration/provider.go b/internal/configuration/provider.go index d6a30f13..6df395fe 100644 --- a/internal/configuration/provider.go +++ b/internal/configuration/provider.go @@ -49,6 +49,8 @@ func (settings *Provider) read(r reader, vpnType string) error { } switch settings.Name { + case constants.Custom: + err = settings.readCustom(r, vpnType) case constants.Cyberghost: err = settings.readCyberghost(r) case constants.Fastestvpn: @@ -99,6 +101,7 @@ func (settings *Provider) readVPNServiceProvider(r reader, vpnType string) (err switch vpnType { case constants.OpenVPN: allowedVPNServiceProviders = []string{ + constants.Custom, "cyberghost", "fastestvpn", "hidemyass", "ipvanish", "ivpn", "mullvad", "nordvpn", "privado", "pia", "private internet access", "privatevpn", "protonvpn", "purevpn", "surfshark", "torguard", constants.VPNUnlimited, "vyprvpn", "windscribe"} @@ -115,6 +118,11 @@ func (settings *Provider) readVPNServiceProvider(r reader, vpnType string) (err if vpnsp == "pia" { // retro compatibility vpnsp = "private internet access" } + + if settings.isOpenVPNCustomConfig(r.env) { // retro compatibility + vpnsp = constants.Custom + } + settings.Name = vpnsp return nil @@ -199,3 +207,12 @@ func portsToString(ports []uint16) string { } return strings.Join(slice, ", ") } + +// isOpenVPNCustomConfig is for retro compatibility to set VPNSP=custom +// if OPENVPN_CUSTOM_CONFIG is set. +func (settings Provider) isOpenVPNCustomConfig(env params.Interface) (ok bool) { + s, _ := env.Get("VPN_TYPE") + isOpenVPN := s == constants.OpenVPN + s, _ = env.Get("OPENVPN_CUSTOM_CONFIG") + return isOpenVPN && s != "" +} diff --git a/internal/configuration/reader.go b/internal/configuration/reader.go index dec6a503..a6b24045 100644 --- a/internal/configuration/reader.go +++ b/internal/configuration/reader.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/qdm12/gluetun/internal/models" + ovpnextract "github.com/qdm12/gluetun/internal/openvpn/extract" "github.com/qdm12/golibs/logging" "github.com/qdm12/golibs/params" "github.com/qdm12/golibs/verification" @@ -18,6 +19,7 @@ type reader struct { env params.Interface logger logging.Logger regex verification.Regex + ovpnExt ovpnextract.Interface } func newReader(env params.Interface, @@ -27,6 +29,7 @@ func newReader(env params.Interface, env: env, logger: logger, regex: verification.NewRegex(), + ovpnExt: ovpnextract.New(), } } diff --git a/internal/configuration/secrets.go b/internal/configuration/secrets.go index 67a45632..5c8118d4 100644 --- a/internal/configuration/secrets.go +++ b/internal/configuration/secrets.go @@ -51,22 +51,22 @@ func (r *reader) getFromEnvOrSecretFile(envKey string, compulsory bool, retroKey file, fileErr := os.OpenFile(filepath, os.O_RDONLY, 0) if os.IsNotExist(fileErr) { if compulsory { - return "", envErr + return "", fmt.Errorf("environment variable %s: %w", envKey, envErr) } return "", nil } else if fileErr != nil { - return "", fmt.Errorf("%w: %s", ErrReadSecretFile, fileErr) + return "", fmt.Errorf("%w: %s: %s", ErrReadSecretFile, filepath, fileErr) } b, err := io.ReadAll(file) if err != nil { - return "", fmt.Errorf("%w: %s", ErrReadSecretFile, err) + return "", fmt.Errorf("%w: %s: %s", ErrReadSecretFile, filepath, err) } value = string(b) value = cleanSuffix(value) if compulsory && value == "" { - return "", ErrSecretFileIsEmpty + return "", fmt.Errorf("%s: %w", filepath, ErrSecretFileIsEmpty) } return value, nil diff --git a/internal/configuration/selection.go b/internal/configuration/selection.go index bb1ce424..352dfa7d 100644 --- a/internal/configuration/selection.go +++ b/internal/configuration/selection.go @@ -106,6 +106,7 @@ func (selection ServerSelection) toLines() (lines []string) { } type OpenVPNSelection struct { + ConfFile string `json:"conf_file"` // Custom configuration file path TCP bool `json:"tcp"` // UDP if TCP is false CustomPort uint16 `json:"custom_port"` // HideMyAss, Mullvad, PIA, ProtonVPN, Windscribe EncPreset string `json:"encryption_preset"` // PIA - needed to get the port number @@ -114,6 +115,10 @@ type OpenVPNSelection struct { func (settings *OpenVPNSelection) lines() (lines []string) { lines = append(lines, lastIndent+"OpenVPN selection:") + if settings.ConfFile != "" { + lines = append(lines, indent+lastIndent+"Custom configuration file: "+settings.ConfFile) + } + lines = append(lines, indent+lastIndent+"Protocol: "+protoToString(settings.TCP)) if settings.CustomPort != 0 { diff --git a/internal/configuration/vpn.go b/internal/configuration/vpn.go index 7056fb33..d09b182e 100644 --- a/internal/configuration/vpn.go +++ b/internal/configuration/vpn.go @@ -58,10 +58,8 @@ func (settings *VPN) read(r reader) (err error) { } settings.Type = vpnType - if !settings.isOpenVPNCustomConfig(r.env) { - if err := settings.Provider.read(r, vpnType); err != nil { - return fmt.Errorf("%w: %s", errReadProviderSettings, err) - } + if err := settings.Provider.read(r, vpnType); err != nil { + return fmt.Errorf("%w: %s", errReadProviderSettings, err) } switch settings.Type { @@ -79,19 +77,3 @@ func (settings *VPN) read(r reader) (err error) { return nil } - -func (settings VPN) isOpenVPNCustomConfig(env params.Interface) (ok bool) { - if settings.Type != constants.OpenVPN { - return false - } - s, err := env.Get("OPENVPN_CUSTOM_CONFIG") - return err == nil && s != "" -} - -func (settings VPN) VPNInterface() (intf string) { - if settings.Type == constants.Wireguard { - return settings.Wireguard.Interface - } - // OpenVPN - return settings.OpenVPN.Interface -} diff --git a/internal/constants/vpn.go b/internal/constants/vpn.go index 9af18ec7..80b245ef 100644 --- a/internal/constants/vpn.go +++ b/internal/constants/vpn.go @@ -6,6 +6,9 @@ const ( ) const ( + // Custom is the VPN provider name for custom + // VPN configurations. + Custom = "custom" // Cyberghost is a VPN provider. Cyberghost = "cyberghost" // Fastestvpn is a VPN provider. diff --git a/internal/openvpn/custom/custom.go b/internal/openvpn/custom/custom.go deleted file mode 100644 index 62c4be52..00000000 --- a/internal/openvpn/custom/custom.go +++ /dev/null @@ -1,35 +0,0 @@ -package custom - -import ( - "errors" - "fmt" - - "github.com/qdm12/gluetun/internal/configuration" - "github.com/qdm12/gluetun/internal/models" -) - -var ( - ErrReadCustomConfig = errors.New("cannot read custom configuration file") - ErrExtractConnection = errors.New("cannot extract connection from custom configuration file") -) - -func BuildConfig(settings configuration.OpenVPN) ( - lines []string, connection models.Connection, intf string, err error) { - lines, err = readCustomConfigLines(settings.Config) - if err != nil { - return nil, connection, "", fmt.Errorf("%w: %s", ErrReadCustomConfig, err) - } - - connection, intf, err = extractDataFromLines(lines) - if err != nil { - return nil, connection, "", fmt.Errorf("%w: %s", ErrExtractConnection, err) - } - - if intf == "" { - intf = settings.Interface - } - - lines = modifyCustomConfig(lines, settings, connection, intf) - - return lines, connection, intf, nil -} diff --git a/internal/openvpn/custom/custom_test.go b/internal/openvpn/custom/custom_test.go deleted file mode 100644 index 34de0a89..00000000 --- a/internal/openvpn/custom/custom_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package custom - -import ( - "net" - "os" - "testing" - - "github.com/qdm12/gluetun/internal/configuration" - "github.com/qdm12/gluetun/internal/constants" - "github.com/qdm12/gluetun/internal/models" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func Test_BuildConfig(t *testing.T) { - t.Parallel() - - file, err := os.CreateTemp("", "") - require.NoError(t, err) - defer removeFile(t, file.Name()) - defer file.Close() - - _, err = file.WriteString("remote 1.9.8.7\nkeep me\ncipher remove") - require.NoError(t, err) - - err = file.Close() - require.NoError(t, err) - - settings := configuration.OpenVPN{ - Cipher: "cipher", - MSSFix: 999, - Config: file.Name(), - Interface: "tun0", - } - - lines, connection, intf, err := BuildConfig(settings) - assert.NoError(t, err) - - expectedLines := []string{ - "keep me", - "proto udp", - "remote 1.9.8.7 1194", - "dev tun0", - "mute-replay-warnings", - "auth-nocache", - "pull-filter ignore \"auth-token\"", - "auth-retry nointeract", - "suppress-timestamps", - "verb 0", - "data-ciphers-fallback cipher", - "data-ciphers cipher", - "mssfix 999", - "pull-filter ignore \"route-ipv6\"", - "pull-filter ignore \"ifconfig-ipv6\"", - "user ", - } - assert.Equal(t, expectedLines, lines) - - expectedConnection := models.Connection{ - IP: net.IPv4(1, 9, 8, 7), - Port: 1194, - Protocol: constants.UDP, - } - assert.Equal(t, expectedConnection, connection) - - assert.Equal(t, "tun0", intf) -} diff --git a/internal/openvpn/extract/data.go b/internal/openvpn/extract/data.go new file mode 100644 index 00000000..b5fcc526 --- /dev/null +++ b/internal/openvpn/extract/data.go @@ -0,0 +1,29 @@ +package extract + +import ( + "errors" + "fmt" + + "github.com/qdm12/gluetun/internal/models" +) + +var ( + ErrRead = errors.New("cannot read file") + ErrExtractConnection = errors.New("cannot extract connection from file") +) + +// Data extracts the lines and connection from the OpenVPN configuration file. +func (e *Extractor) Data(filepath string) (lines []string, + connection models.Connection, err error) { + lines, err = readCustomConfigLines(filepath) + if err != nil { + return nil, connection, fmt.Errorf("%w: %s", ErrRead, err) + } + + connection, err = extractDataFromLines(lines) + if err != nil { + return nil, connection, fmt.Errorf("%w: %s", ErrExtractConnection, err) + } + + return lines, connection, nil +} diff --git a/internal/openvpn/custom/extract.go b/internal/openvpn/extract/extract.go similarity index 68% rename from internal/openvpn/custom/extract.go rename to internal/openvpn/extract/extract.go index 1e022a83..19edd5f1 100644 --- a/internal/openvpn/custom/extract.go +++ b/internal/openvpn/extract/extract.go @@ -1,4 +1,4 @@ -package custom +package extract import ( "errors" @@ -16,23 +16,22 @@ var ( ) func extractDataFromLines(lines []string) ( - connection models.Connection, intf string, err error) { + connection models.Connection, err error) { for i, line := range lines { - ip, port, protocol, intfFound, err := extractDataFromLine(line) + ip, port, protocol, err := extractDataFromLine(line) if err != nil { - return connection, "", fmt.Errorf("on line %d: %w", i+1, err) + return connection, fmt.Errorf("on line %d: %w", i+1, err) } - intf = intfFound connection.UpdateEmptyWith(ip, port, protocol) - if connection.Protocol != "" && connection.IP != nil && intf != "" { + if connection.Protocol != "" && connection.IP != nil { break } } if connection.IP == nil { - return connection, "", errRemoteLineNotFound + return connection, errRemoteLineNotFound } if connection.Protocol == "" { @@ -46,41 +45,33 @@ func extractDataFromLines(lines []string) ( } } - return connection, intf, nil + return connection, nil } var ( errExtractProto = errors.New("failed extracting protocol from proto line") errExtractRemote = errors.New("failed extracting from remote line") - errExtractDev = errors.New("failed extracting network interface from dev line") ) func extractDataFromLine(line string) ( - ip net.IP, port uint16, protocol, intf string, err error) { + ip net.IP, port uint16, protocol string, err error) { switch { case strings.HasPrefix(line, "proto "): protocol, err = extractProto(line) if err != nil { - return nil, 0, "", "", fmt.Errorf("%w: %s", errExtractProto, err) + return nil, 0, "", fmt.Errorf("%w: %s", errExtractProto, err) } - return nil, 0, protocol, "", nil + return nil, 0, protocol, nil case strings.HasPrefix(line, "remote "): ip, port, protocol, err = extractRemote(line) if err != nil { - return nil, 0, "", "", fmt.Errorf("%w: %s", errExtractRemote, err) + return nil, 0, "", fmt.Errorf("%w: %s", errExtractRemote, err) } - return ip, port, protocol, "", nil - - case strings.HasPrefix(line, "dev "): - intf, err = extractInterfaceFromLine(line) - if err != nil { - return nil, 0, "", "", fmt.Errorf("%w: %s", errExtractDev, err) - } - return nil, 0, "", intf, nil + return ip, port, protocol, nil } - return nil, 0, "", "", nil + return nil, 0, "", nil } var ( @@ -147,16 +138,3 @@ func extractRemote(line string) (ip net.IP, port uint16, return ip, port, protocol, nil } - -var ( - errDevLineFieldsCount = errors.New("dev line has not 2 fields as expected") -) - -func extractInterfaceFromLine(line string) (intf string, err error) { - fields := strings.Fields(line) - if len(fields) != 2 { //nolint:gomnd - return "", fmt.Errorf("%w: %s", errDevLineFieldsCount, line) - } - - return fields[1], nil -} diff --git a/internal/openvpn/custom/extract_test.go b/internal/openvpn/extract/extract_test.go similarity index 82% rename from internal/openvpn/custom/extract_test.go rename to internal/openvpn/extract/extract_test.go index 24ea8749..b5191422 100644 --- a/internal/openvpn/custom/extract_test.go +++ b/internal/openvpn/extract/extract_test.go @@ -1,4 +1,4 @@ -package custom +package extract import ( "errors" @@ -17,7 +17,6 @@ func Test_extractDataFromLines(t *testing.T) { testCases := map[string]struct { lines []string connection models.Connection - intf string err error }{ "success": { @@ -27,7 +26,6 @@ func Test_extractDataFromLines(t *testing.T) { Port: 1194, Protocol: constants.TCP, }, - intf: "tun6", }, "extraction error": { lines: []string{"bla bla", "proto bad", "remote 1.2.3.4 1194 tcp"}, @@ -71,7 +69,7 @@ func Test_extractDataFromLines(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - connection, intf, err := extractDataFromLines(testCase.lines) + connection, err := extractDataFromLines(testCase.lines) if testCase.err != nil { require.Error(t, err) @@ -81,7 +79,6 @@ func Test_extractDataFromLines(t *testing.T) { } assert.Equal(t, testCase.connection, connection) - assert.Equal(t, testCase.intf, intf) }) } } @@ -94,7 +91,6 @@ func Test_extractDataFromLine(t *testing.T) { ip net.IP port uint16 protocol string - intf string isErr error }{ "irrelevant line": { @@ -108,14 +104,6 @@ func Test_extractDataFromLine(t *testing.T) { line: "proto tcp", protocol: constants.TCP, }, - "extract intf error": { - line: "dev ", - isErr: errExtractDev, - }, - "extract intf success": { - line: "dev tun3", - intf: "tun3", - }, "extract remote error": { line: "remote bad", isErr: errExtractRemote, @@ -133,7 +121,7 @@ func Test_extractDataFromLine(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - ip, port, protocol, intf, err := extractDataFromLine(testCase.line) + ip, port, protocol, err := extractDataFromLine(testCase.line) if testCase.isErr != nil { assert.ErrorIs(t, err, testCase.isErr) @@ -144,7 +132,6 @@ func Test_extractDataFromLine(t *testing.T) { assert.Equal(t, testCase.ip, ip) assert.Equal(t, testCase.port, port) assert.Equal(t, testCase.protocol, protocol) - assert.Equal(t, testCase.intf, intf) }) } } @@ -273,44 +260,3 @@ func Test_extractRemote(t *testing.T) { }) } } - -func Test_extractInterface(t *testing.T) { - t.Parallel() - - testCases := map[string]struct { - line string - intf string - err error - }{ - "found": { - line: "dev tun3", - intf: "tun3", - }, - "not enough fields": { - line: "dev ", - err: errors.New("dev line has not 2 fields as expected: dev "), - }, - "too many fields": { - line: "dev one two", - err: errors.New("dev line has not 2 fields as expected: dev one two"), - }, - } - - for name, testCase := range testCases { - testCase := testCase - t.Run(name, func(t *testing.T) { - t.Parallel() - - intf, err := extractInterfaceFromLine(testCase.line) - - 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.intf, intf) - }) - } -} diff --git a/internal/openvpn/extract/extractor.go b/internal/openvpn/extract/extractor.go new file mode 100644 index 00000000..a40614aa --- /dev/null +++ b/internal/openvpn/extract/extractor.go @@ -0,0 +1,18 @@ +package extract + +import ( + "github.com/qdm12/gluetun/internal/models" +) + +var _ Interface = (*Extractor)(nil) + +type Interface interface { + Data(filepath string) (lines []string, + connection models.Connection, err error) +} + +type Extractor struct{} + +func New() *Extractor { + return new(Extractor) +} diff --git a/internal/openvpn/custom/helpers_test.go b/internal/openvpn/extract/helpers_test.go similarity index 92% rename from internal/openvpn/custom/helpers_test.go rename to internal/openvpn/extract/helpers_test.go index b6f6df2e..41c6c174 100644 --- a/internal/openvpn/custom/helpers_test.go +++ b/internal/openvpn/extract/helpers_test.go @@ -1,4 +1,4 @@ -package custom +package extract import ( "os" diff --git a/internal/openvpn/custom/read.go b/internal/openvpn/extract/read.go similarity index 95% rename from internal/openvpn/custom/read.go rename to internal/openvpn/extract/read.go index 8e2ee03d..9da84948 100644 --- a/internal/openvpn/custom/read.go +++ b/internal/openvpn/extract/read.go @@ -1,4 +1,4 @@ -package custom +package extract import ( "io" diff --git a/internal/openvpn/custom/read_test.go b/internal/openvpn/extract/read_test.go similarity index 97% rename from internal/openvpn/custom/read_test.go rename to internal/openvpn/extract/read_test.go index bc050427..e2bb2981 100644 --- a/internal/openvpn/custom/read_test.go +++ b/internal/openvpn/extract/read_test.go @@ -1,4 +1,4 @@ -package custom +package extract import ( "os" diff --git a/internal/provider/custom/connection.go b/internal/provider/custom/connection.go new file mode 100644 index 00000000..6713c02b --- /dev/null +++ b/internal/provider/custom/connection.go @@ -0,0 +1,38 @@ +package custom + +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 ( + ErrVPNTypeNotSupported = errors.New("VPN type not supported for custom provider") + ErrExtractConnection = errors.New("cannot extract connection") +) + +// GetConnection gets the connection from the OpenVPN configuration file. +func (p *Provider) GetConnection(selection configuration.ServerSelection) ( + connection models.Connection, err error) { + if selection.VPN != constants.OpenVPN { + return connection, fmt.Errorf("%w: %s", ErrVPNTypeNotSupported, selection.VPN) + } + + _, connection, err = p.extractor.Data(selection.OpenVPN.ConfFile) + if err != nil { + return connection, fmt.Errorf("%w: %s", ErrExtractConnection, err) + } + + connection.Port = getPort(connection.Port, selection) + + return connection, nil +} + +// Port found is overridden by custom port set with `PORT` or `WIREGUARD_PORT`. +func getPort(foundPort uint16, selection configuration.ServerSelection) (port uint16) { + return utils.GetPort(selection, foundPort, foundPort, foundPort) +} diff --git a/internal/openvpn/custom/modify.go b/internal/provider/custom/openvpnconf.go similarity index 67% rename from internal/openvpn/custom/modify.go rename to internal/provider/custom/openvpnconf.go index 490b1e40..82eae215 100644 --- a/internal/openvpn/custom/modify.go +++ b/internal/provider/custom/openvpnconf.go @@ -1,6 +1,8 @@ package custom import ( + "errors" + "fmt" "strconv" "strings" @@ -10,13 +12,27 @@ import ( "github.com/qdm12/gluetun/internal/provider/utils" ) -func modifyCustomConfig(lines []string, settings configuration.OpenVPN, - connection models.Connection, intf string) (modified []string) { +var ErrExtractData = errors.New("failed extracting information from custom configuration file") + +func (p *Provider) BuildConf(connection models.Connection, + settings configuration.OpenVPN) (lines []string, err error) { + lines, _, err = p.extractor.Data(settings.ConfFile) + if err != nil { + return nil, fmt.Errorf("%w: %s", ErrExtractData, err) + } + + lines = modifyConfig(lines, connection, settings) + + return lines, nil +} + +func modifyConfig(lines []string, connection models.Connection, + settings configuration.OpenVPN) (modified []string) { // Remove some lines for _, line := range lines { switch { - case strings.HasPrefix(line, "up "), - strings.HasPrefix(line, "down "), + case + line == "", strings.HasPrefix(line, "verb "), strings.HasPrefix(line, "auth-user-pass "), strings.HasPrefix(line, "user "), @@ -36,7 +52,7 @@ func modifyCustomConfig(lines []string, settings configuration.OpenVPN, // Add values modified = append(modified, connection.OpenVPNProtoLine()) modified = append(modified, connection.OpenVPNRemoteLine()) - modified = append(modified, "dev "+intf) + modified = append(modified, "dev "+settings.Interface) modified = append(modified, "mute-replay-warnings") modified = append(modified, "auth-nocache") modified = append(modified, "pull-filter ignore \"auth-token\"") // prevent auth failed loop @@ -63,5 +79,23 @@ func modifyCustomConfig(lines []string, settings configuration.OpenVPN, modified = append(modified, "user "+settings.ProcUser) } - return modified + modified = append(modified, "") // trailing line + + return uniqueLines(modified) +} + +func uniqueLines(lines []string) (unique []string) { + seen := make(map[string]struct{}, len(lines)) + unique = make([]string, 0, len(lines)) + + for _, line := range lines { + _, ok := seen[line] + if ok { + continue + } + seen[line] = struct{}{} + unique = append(unique, line) + } + + return unique } diff --git a/internal/openvpn/custom/modify_test.go b/internal/provider/custom/openvpnconf_test.go similarity index 82% rename from internal/openvpn/custom/modify_test.go rename to internal/provider/custom/openvpnconf_test.go index 493e7c41..702b40e4 100644 --- a/internal/openvpn/custom/modify_test.go +++ b/internal/provider/custom/openvpnconf_test.go @@ -10,14 +10,13 @@ import ( "github.com/stretchr/testify/assert" ) -func Test_modifyCustomConfig(t *testing.T) { +func Test_modifyConfig(t *testing.T) { t.Parallel() testCases := map[string]struct { lines []string settings configuration.OpenVPN connection models.Connection - intf string modified []string }{ "mixed": { @@ -26,24 +25,26 @@ func Test_modifyCustomConfig(t *testing.T) { "proto tcp", "remote 5.5.5.5", "cipher bla", + "", "tun-ipv6", "keep me here", "auth bla", }, settings: configuration.OpenVPN{ - User: "user", - Cipher: "cipher", - Auth: "auth", - MSSFix: 1000, - ProcUser: "procuser", + User: "user", + Cipher: "cipher", + Auth: "auth", + MSSFix: 1000, + ProcUser: "procuser", + Interface: "tun3", }, connection: models.Connection{ IP: net.IPv4(1, 2, 3, 4), Port: 1194, Protocol: constants.UDP, }, - intf: "tun3", modified: []string{ + "up bla", "keep me here", "proto udp", "remote 1.2.3.4 1194", @@ -62,6 +63,7 @@ func Test_modifyCustomConfig(t *testing.T) { "pull-filter ignore \"route-ipv6\"", "pull-filter ignore \"ifconfig-ipv6\"", "user procuser", + "", }, }, } @@ -71,8 +73,8 @@ func Test_modifyCustomConfig(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - modified := modifyCustomConfig(testCase.lines, - testCase.settings, testCase.connection, testCase.intf) + modified := modifyConfig(testCase.lines, + testCase.connection, testCase.settings) assert.Equal(t, testCase.modified, modified) }) diff --git a/internal/provider/custom/provider.go b/internal/provider/custom/provider.go new file mode 100644 index 00000000..28dab95d --- /dev/null +++ b/internal/provider/custom/provider.go @@ -0,0 +1,19 @@ +package custom + +import ( + "github.com/qdm12/gluetun/internal/constants" + "github.com/qdm12/gluetun/internal/openvpn/extract" + "github.com/qdm12/gluetun/internal/provider/utils" +) + +type Provider struct { + extractor extract.Interface + utils.NoPortForwarder +} + +func New() *Provider { + return &Provider{ + extractor: extract.New(), + NoPortForwarder: utils.NewNoPortForwarding(constants.Custom), + } +} diff --git a/internal/provider/cyberghost/openvpnconf.go b/internal/provider/cyberghost/openvpnconf.go index 231f5df8..37cf2767 100644 --- a/internal/provider/cyberghost/openvpnconf.go +++ b/internal/provider/cyberghost/openvpnconf.go @@ -11,7 +11,7 @@ import ( ) func (c *Cyberghost) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string) { + settings configuration.OpenVPN) (lines []string, err error) { if settings.Cipher == "" { settings.Cipher = constants.AES256cbc } @@ -87,5 +87,5 @@ func (c *Cyberghost) BuildConf(connection models.Connection, lines = append(lines, "") - return lines + return lines, nil } diff --git a/internal/provider/fastestvpn/openvpnconf.go b/internal/provider/fastestvpn/openvpnconf.go index 105f434f..62fc7cc3 100644 --- a/internal/provider/fastestvpn/openvpnconf.go +++ b/internal/provider/fastestvpn/openvpnconf.go @@ -10,7 +10,7 @@ import ( ) func (f *Fastestvpn) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string) { + settings configuration.OpenVPN) (lines []string, err error) { if settings.Cipher == "" { settings.Cipher = constants.AES256cbc } @@ -76,5 +76,5 @@ func (f *Fastestvpn) BuildConf(connection models.Connection, lines = append(lines, "") - return lines + return lines, nil } diff --git a/internal/provider/hidemyass/openvpnconf.go b/internal/provider/hidemyass/openvpnconf.go index 6b898fec..59592502 100644 --- a/internal/provider/hidemyass/openvpnconf.go +++ b/internal/provider/hidemyass/openvpnconf.go @@ -10,7 +10,7 @@ import ( ) func (h *HideMyAss) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string) { + settings configuration.OpenVPN) (lines []string, err error) { if settings.Cipher == "" { settings.Cipher = constants.AES256cbc } @@ -75,5 +75,5 @@ func (h *HideMyAss) BuildConf(connection models.Connection, lines = append(lines, "") - return lines + return lines, nil } diff --git a/internal/provider/ipvanish/openvpnconf.go b/internal/provider/ipvanish/openvpnconf.go index 5065204f..afd7ab6f 100644 --- a/internal/provider/ipvanish/openvpnconf.go +++ b/internal/provider/ipvanish/openvpnconf.go @@ -10,7 +10,7 @@ import ( ) func (i *Ipvanish) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string) { + settings configuration.OpenVPN) (lines []string, err error) { if settings.Cipher == "" { settings.Cipher = constants.AES256cbc } @@ -68,5 +68,5 @@ func (i *Ipvanish) BuildConf(connection models.Connection, lines = append(lines, "") - return lines + return lines, nil } diff --git a/internal/provider/ivpn/openvpnconf.go b/internal/provider/ivpn/openvpnconf.go index 41307365..e72ee5c5 100644 --- a/internal/provider/ivpn/openvpnconf.go +++ b/internal/provider/ivpn/openvpnconf.go @@ -11,7 +11,7 @@ import ( ) func (i *Ivpn) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string) { + settings configuration.OpenVPN) (lines []string, err error) { if settings.Cipher == "" { settings.Cipher = constants.AES256cbc } @@ -77,5 +77,5 @@ func (i *Ivpn) BuildConf(connection models.Connection, lines = append(lines, "") - return lines + return lines, nil } diff --git a/internal/provider/mullvad/openvpnconf.go b/internal/provider/mullvad/openvpnconf.go index 20449880..15fab6c6 100644 --- a/internal/provider/mullvad/openvpnconf.go +++ b/internal/provider/mullvad/openvpnconf.go @@ -10,7 +10,7 @@ import ( ) func (m *Mullvad) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string) { + settings configuration.OpenVPN) (lines []string, err error) { if settings.Cipher == "" { settings.Cipher = constants.AES256cbc } @@ -83,5 +83,5 @@ func (m *Mullvad) BuildConf(connection models.Connection, lines = append(lines, "") - return lines + return lines, nil } diff --git a/internal/provider/nordvpn/openvpnconf.go b/internal/provider/nordvpn/openvpnconf.go index dd926ba1..d64b4ece 100644 --- a/internal/provider/nordvpn/openvpnconf.go +++ b/internal/provider/nordvpn/openvpnconf.go @@ -10,7 +10,7 @@ import ( ) func (n *Nordvpn) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string) { + settings configuration.OpenVPN) (lines []string, err error) { if settings.Cipher == "" { settings.Cipher = constants.AES256cbc } @@ -81,5 +81,5 @@ func (n *Nordvpn) BuildConf(connection models.Connection, lines = append(lines, "") - return lines + return lines, nil } diff --git a/internal/provider/privado/openvpnconf.go b/internal/provider/privado/openvpnconf.go index 0c0af77e..cb20ffd4 100644 --- a/internal/provider/privado/openvpnconf.go +++ b/internal/provider/privado/openvpnconf.go @@ -10,7 +10,7 @@ import ( ) func (p *Privado) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string) { + settings configuration.OpenVPN) (lines []string, err error) { if settings.Cipher == "" { settings.Cipher = constants.AES256cbc } @@ -70,5 +70,5 @@ func (p *Privado) BuildConf(connection models.Connection, lines = append(lines, "") - return lines + return lines, nil } diff --git a/internal/provider/privateinternetaccess/openvpnconf.go b/internal/provider/privateinternetaccess/openvpnconf.go index 78222f67..400b4d99 100644 --- a/internal/provider/privateinternetaccess/openvpnconf.go +++ b/internal/provider/privateinternetaccess/openvpnconf.go @@ -10,7 +10,7 @@ import ( ) func (p *PIA) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string) { + settings configuration.OpenVPN) (lines []string, err error) { var defaultCipher, defaultAuth, X509CRL, certificate string switch settings.EncPreset { case constants.PIAEncryptionPresetNormal: @@ -93,5 +93,5 @@ func (p *PIA) BuildConf(connection models.Connection, lines = append(lines, "") - return lines + return lines, nil } diff --git a/internal/provider/privatevpn/openvpnconf.go b/internal/provider/privatevpn/openvpnconf.go index fdb3194f..8cde04be 100644 --- a/internal/provider/privatevpn/openvpnconf.go +++ b/internal/provider/privatevpn/openvpnconf.go @@ -10,7 +10,7 @@ import ( ) func (p *Privatevpn) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string) { + settings configuration.OpenVPN) (lines []string, err error) { if settings.Cipher == "" { settings.Cipher = constants.AES128gcm } @@ -73,5 +73,5 @@ func (p *Privatevpn) BuildConf(connection models.Connection, lines = append(lines, "") - return lines + return lines, nil } diff --git a/internal/provider/protonvpn/openvpnconf.go b/internal/provider/protonvpn/openvpnconf.go index 357b4e0c..1828f13c 100644 --- a/internal/provider/protonvpn/openvpnconf.go +++ b/internal/provider/protonvpn/openvpnconf.go @@ -10,7 +10,7 @@ import ( ) func (p *Protonvpn) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string) { + settings configuration.OpenVPN) (lines []string, err error) { if settings.Cipher == "" { settings.Cipher = constants.AES256cbc } @@ -80,5 +80,5 @@ func (p *Protonvpn) BuildConf(connection models.Connection, lines = append(lines, "") - return lines + return lines, nil } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 6ede2f80..c0fa2d7c 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -11,6 +11,7 @@ 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/custom" "github.com/qdm12/gluetun/internal/provider/cyberghost" "github.com/qdm12/gluetun/internal/provider/fastestvpn" "github.com/qdm12/gluetun/internal/provider/hidemyass" @@ -34,7 +35,7 @@ import ( // Provider contains methods to read and modify the openvpn configuration to connect as a client. type Provider interface { GetConnection(selection configuration.ServerSelection) (connection models.Connection, err error) - BuildConf(connection models.Connection, settings configuration.OpenVPN) (lines []string) + BuildConf(connection models.Connection, settings configuration.OpenVPN) (lines []string, err error) PortForwarder } @@ -50,6 +51,8 @@ type PortForwarder interface { func New(provider string, allServers models.AllServers, timeNow func() time.Time) Provider { randSource := rand.NewSource(timeNow().UnixNano()) switch provider { + case constants.Custom: + return custom.New() case constants.Cyberghost: return cyberghost.New(allServers.Cyberghost.Servers, randSource) case constants.Fastestvpn: diff --git a/internal/provider/purevpn/openvpnconf.go b/internal/provider/purevpn/openvpnconf.go index db83de4a..58d3ee22 100644 --- a/internal/provider/purevpn/openvpnconf.go +++ b/internal/provider/purevpn/openvpnconf.go @@ -10,7 +10,7 @@ import ( ) func (p *Purevpn) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string) { + settings configuration.OpenVPN) (lines []string, err error) { if settings.Cipher == "" { settings.Cipher = constants.AES256gcm } @@ -84,5 +84,5 @@ func (p *Purevpn) BuildConf(connection models.Connection, lines = append(lines, "") - return lines + return lines, nil } diff --git a/internal/provider/surfshark/openvpnconf.go b/internal/provider/surfshark/openvpnconf.go index 58b5892f..6299406c 100644 --- a/internal/provider/surfshark/openvpnconf.go +++ b/internal/provider/surfshark/openvpnconf.go @@ -10,7 +10,7 @@ import ( ) func (s *Surfshark) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string) { + settings configuration.OpenVPN) (lines []string, err error) { if settings.Cipher == "" { settings.Cipher = constants.AES256gcm } @@ -78,5 +78,5 @@ func (s *Surfshark) BuildConf(connection models.Connection, lines = append(lines, "") - return lines + return lines, nil } diff --git a/internal/provider/torguard/openvpnconf.go b/internal/provider/torguard/openvpnconf.go index 8ea3a6aa..27924df6 100644 --- a/internal/provider/torguard/openvpnconf.go +++ b/internal/provider/torguard/openvpnconf.go @@ -10,7 +10,7 @@ import ( ) func (t *Torguard) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string) { + settings configuration.OpenVPN) (lines []string, err error) { if settings.Cipher == "" { settings.Cipher = constants.AES256gcm } @@ -84,5 +84,5 @@ func (t *Torguard) BuildConf(connection models.Connection, lines = append(lines, "") - return lines + return lines, nil } diff --git a/internal/provider/vpnunlimited/openvpnconf.go b/internal/provider/vpnunlimited/openvpnconf.go index f2c65623..2f3168db 100644 --- a/internal/provider/vpnunlimited/openvpnconf.go +++ b/internal/provider/vpnunlimited/openvpnconf.go @@ -10,7 +10,7 @@ import ( ) func (p *Provider) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string) { + settings configuration.OpenVPN) (lines []string, err error) { lines = []string{ "client", "dev " + settings.Interface, @@ -71,5 +71,5 @@ func (p *Provider) BuildConf(connection models.Connection, lines = append(lines, "") - return lines + return lines, nil } diff --git a/internal/provider/vyprvpn/openvpnconf.go b/internal/provider/vyprvpn/openvpnconf.go index 1d8e4da4..a97229b1 100644 --- a/internal/provider/vyprvpn/openvpnconf.go +++ b/internal/provider/vyprvpn/openvpnconf.go @@ -10,7 +10,7 @@ import ( ) func (v *Vyprvpn) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string) { + settings configuration.OpenVPN) (lines []string, err error) { if settings.Cipher == "" { settings.Cipher = constants.AES256cbc } @@ -65,5 +65,5 @@ func (v *Vyprvpn) BuildConf(connection models.Connection, lines = append(lines, "") - return lines + return lines, nil } diff --git a/internal/provider/windscribe/openvpnconf.go b/internal/provider/windscribe/openvpnconf.go index 33921c52..9e8afb4f 100644 --- a/internal/provider/windscribe/openvpnconf.go +++ b/internal/provider/windscribe/openvpnconf.go @@ -11,7 +11,7 @@ import ( ) func (w *Windscribe) BuildConf(connection models.Connection, - settings configuration.OpenVPN) (lines []string) { + settings configuration.OpenVPN) (lines []string, err error) { if settings.Cipher == "" { settings.Cipher = constants.AES256cbc } @@ -81,5 +81,5 @@ func (w *Windscribe) BuildConf(connection models.Connection, lines = append(lines, "") - return lines + return lines, nil } diff --git a/internal/vpn/openvpn.go b/internal/vpn/openvpn.go index 0250af38..48d3a537 100644 --- a/internal/vpn/openvpn.go +++ b/internal/vpn/openvpn.go @@ -7,15 +7,14 @@ import ( "github.com/qdm12/gluetun/internal/configuration" "github.com/qdm12/gluetun/internal/firewall" - "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/openvpn" - "github.com/qdm12/gluetun/internal/openvpn/custom" "github.com/qdm12/gluetun/internal/provider" "github.com/qdm12/golibs/command" "github.com/qdm12/golibs/logging" ) var ( + errServerConn = errors.New("failed finding a valid server connection") errBuildConfig = errors.New("failed building configuration") errWriteConfig = errors.New("failed writing configuration to file") errWriteAuth = errors.New("failed writing auth to file") @@ -28,18 +27,12 @@ func setupOpenVPN(ctx context.Context, fw firewall.VPNConnectionSetter, openvpnConf openvpn.Interface, providerConf provider.Provider, settings configuration.VPN, starter command.Starter, logger logging.Logger) ( runner vpnRunner, serverName string, err error) { - var connection models.Connection - var netInterface string - var lines []string - if settings.OpenVPN.Config == "" { - netInterface = settings.OpenVPN.Interface - connection, err = providerConf.GetConnection(settings.Provider.ServerSelection) - if err == nil { - lines = providerConf.BuildConf(connection, settings.OpenVPN) - } - } else { - lines, connection, netInterface, err = custom.BuildConfig(settings.OpenVPN) + connection, err := providerConf.GetConnection(settings.Provider.ServerSelection) + if err != nil { + return nil, "", fmt.Errorf("%w: %s", errServerConn, err) } + + lines, err := providerConf.BuildConf(connection, settings.OpenVPN) if err != nil { return nil, "", fmt.Errorf("%w: %s", errBuildConfig, err) } @@ -55,7 +48,7 @@ func setupOpenVPN(ctx context.Context, fw firewall.VPNConnectionSetter, } } - if err := fw.SetVPNConnection(ctx, connection, netInterface); err != nil { + if err := fw.SetVPNConnection(ctx, connection, settings.OpenVPN.Interface); err != nil { return nil, "", fmt.Errorf("%w: %s", errFirewall, err) } diff --git a/maintenance.md b/maintenance.md index a6517868..8aaef98b 100644 --- a/maintenance.md +++ b/maintenance.md @@ -28,6 +28,7 @@ - Gluetun entire logs available at control server, maybe in structured format - Authentication with the control server - Get announcement from Github file +- Support multiple connections in custom ovpn ## Gluetun V4 @@ -47,4 +48,7 @@ - Change `VPNSP` to `VPN_SERVICE_PROVIDER` - Change `REGION` (etc.) to `SERVER_REGIONS` - Remove `PUBLICIP_FILE` +- Remove retro-compatibility where OPENVPN_CONFIG != "" implies VPNSP = "custom" + and set `OPENVPN_CUSTOM_CONFIG` default to `/gluetun/custom.ovpn` - Split servers.json and compress it +- Use relative paths everywhere instead of absolute