VPNSP value custom for OpenVPN custom config files (#621)

- Retro-compatibility: `OPENVPN_CUSTOM_CONFIG` set implies `VPNSP=custom`
- Change: `up` and `down` options are not filtered out
- Change: `OPENVPN_INTERFACE` overrides the network interface defined in the configuration file
- Change: `PORT` overrides any port found in the configuration file
- Feat: config file is read when building the OpenVPN configuration, so it's effectively reloaded on VPN restarts
- Feat: extract values from custom file at start to log out valid settings
- Maint: `internal/openvpn/extract` package instead of `internal/openvpn/custom` package
- Maint: All providers' `BuildConf` method return an error
- Maint: rename `CustomConfig` to `ConfFile` in Settings structures
This commit is contained in:
Quentin McGaw
2021-09-13 11:30:14 -04:00
committed by GitHub
parent 11af6c10f1
commit f807f756eb
43 changed files with 328 additions and 296 deletions

View File

@@ -34,7 +34,11 @@ func (c *CLI) OpenvpnConfig(logger logging.Logger, env params.Interface) error {
if err != nil { if err != nil {
return err 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")) fmt.Println(strings.Join(lines, "\n"))
return nil return nil
} }

View File

@@ -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
}

View File

@@ -21,7 +21,7 @@ type OpenVPN struct {
Root bool `json:"run_as_root"` Root bool `json:"run_as_root"`
Cipher string `json:"cipher"` Cipher string `json:"cipher"`
Auth string `json:"auth"` Auth string `json:"auth"`
Config string `json:"custom_config"` ConfFile string `json:"conf_file"`
Version string `json:"version"` Version string `json:"version"`
ClientCrt string `json:"-"` // Cyberghost ClientCrt string `json:"-"` // Cyberghost
ClientKey string `json:"-"` // Cyberghost, VPNUnlimited 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) lines = append(lines, indent+lastIndent+"Custom auth algorithm: "+settings.Auth)
} }
if len(settings.Config) > 0 { if settings.ConfFile != "" {
lines = append(lines, indent+lastIndent+"Custom configuration: "+settings.Config) lines = append(lines, indent+lastIndent+"Configuration file: "+settings.ConfFile)
} }
if settings.ClientKey != "" { if settings.ClientKey != "" {
@@ -83,13 +83,14 @@ func (settings *OpenVPN) lines() (lines []string) {
} }
func (settings *OpenVPN) read(r reader, serviceProvider string) (err error) { func (settings *OpenVPN) read(r reader, serviceProvider string) (err error) {
settings.Config, err = r.env.Get("OPENVPN_CUSTOM_CONFIG", params.CaseSensitiveValue()) credentialsRequired := false
if err != nil { switch serviceProvider {
return fmt.Errorf("environment variable OPENVPN_CUSTOM_CONFIG: %w", err) 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"}) settings.User, err = r.getFromEnvOrSecretFile("OPENVPN_USER", credentialsRequired, []string{"USER"})
if err != nil { if err != nil {
return fmt.Errorf("environment variable OPENVPN_USER: %w", err) 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 { switch serviceProvider {
case constants.Custom:
err = settings.readCustom(r) // read OPENVPN_CUSTOM_CONFIG
case constants.Cyberghost: case constants.Cyberghost:
err = settings.readCyberghost(r) err = settings.readCyberghost(r)
case constants.PrivateInternetAccess: case constants.PrivateInternetAccess:

View File

@@ -25,7 +25,7 @@ func Test_OpenVPN_JSON(t *testing.T) {
"run_as_root": true, "run_as_root": true,
"cipher": "", "cipher": "",
"auth": "", "auth": "",
"custom_config": "", "conf_file": "",
"version": "", "version": "",
"encryption_preset": "", "encryption_preset": "",
"ipv6": false, "ipv6": false,

View File

@@ -49,6 +49,8 @@ func (settings *Provider) read(r reader, vpnType string) error {
} }
switch settings.Name { switch settings.Name {
case constants.Custom:
err = settings.readCustom(r, vpnType)
case constants.Cyberghost: case constants.Cyberghost:
err = settings.readCyberghost(r) err = settings.readCyberghost(r)
case constants.Fastestvpn: case constants.Fastestvpn:
@@ -99,6 +101,7 @@ func (settings *Provider) readVPNServiceProvider(r reader, vpnType string) (err
switch vpnType { switch vpnType {
case constants.OpenVPN: case constants.OpenVPN:
allowedVPNServiceProviders = []string{ allowedVPNServiceProviders = []string{
constants.Custom,
"cyberghost", "fastestvpn", "hidemyass", "ipvanish", "ivpn", "mullvad", "nordvpn", "cyberghost", "fastestvpn", "hidemyass", "ipvanish", "ivpn", "mullvad", "nordvpn",
"privado", "pia", "private internet access", "privatevpn", "protonvpn", "privado", "pia", "private internet access", "privatevpn", "protonvpn",
"purevpn", "surfshark", "torguard", constants.VPNUnlimited, "vyprvpn", "windscribe"} "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 if vpnsp == "pia" { // retro compatibility
vpnsp = "private internet access" vpnsp = "private internet access"
} }
if settings.isOpenVPNCustomConfig(r.env) { // retro compatibility
vpnsp = constants.Custom
}
settings.Name = vpnsp settings.Name = vpnsp
return nil return nil
@@ -199,3 +207,12 @@ func portsToString(ports []uint16) string {
} }
return strings.Join(slice, ", ") 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 != ""
}

View File

@@ -8,6 +8,7 @@ import (
"strings" "strings"
"github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/models"
ovpnextract "github.com/qdm12/gluetun/internal/openvpn/extract"
"github.com/qdm12/golibs/logging" "github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/params" "github.com/qdm12/golibs/params"
"github.com/qdm12/golibs/verification" "github.com/qdm12/golibs/verification"
@@ -18,6 +19,7 @@ type reader struct {
env params.Interface env params.Interface
logger logging.Logger logger logging.Logger
regex verification.Regex regex verification.Regex
ovpnExt ovpnextract.Interface
} }
func newReader(env params.Interface, func newReader(env params.Interface,
@@ -27,6 +29,7 @@ func newReader(env params.Interface,
env: env, env: env,
logger: logger, logger: logger,
regex: verification.NewRegex(), regex: verification.NewRegex(),
ovpnExt: ovpnextract.New(),
} }
} }

View File

@@ -51,22 +51,22 @@ func (r *reader) getFromEnvOrSecretFile(envKey string, compulsory bool, retroKey
file, fileErr := os.OpenFile(filepath, os.O_RDONLY, 0) file, fileErr := os.OpenFile(filepath, os.O_RDONLY, 0)
if os.IsNotExist(fileErr) { if os.IsNotExist(fileErr) {
if compulsory { if compulsory {
return "", envErr return "", fmt.Errorf("environment variable %s: %w", envKey, envErr)
} }
return "", nil return "", nil
} else if fileErr != 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) b, err := io.ReadAll(file)
if err != nil { if err != nil {
return "", fmt.Errorf("%w: %s", ErrReadSecretFile, err) return "", fmt.Errorf("%w: %s: %s", ErrReadSecretFile, filepath, err)
} }
value = string(b) value = string(b)
value = cleanSuffix(value) value = cleanSuffix(value)
if compulsory && value == "" { if compulsory && value == "" {
return "", ErrSecretFileIsEmpty return "", fmt.Errorf("%s: %w", filepath, ErrSecretFileIsEmpty)
} }
return value, nil return value, nil

View File

@@ -106,6 +106,7 @@ func (selection ServerSelection) toLines() (lines []string) {
} }
type OpenVPNSelection struct { type OpenVPNSelection struct {
ConfFile string `json:"conf_file"` // Custom configuration file path
TCP bool `json:"tcp"` // UDP if TCP is false TCP bool `json:"tcp"` // UDP if TCP is false
CustomPort uint16 `json:"custom_port"` // HideMyAss, Mullvad, PIA, ProtonVPN, Windscribe CustomPort uint16 `json:"custom_port"` // HideMyAss, Mullvad, PIA, ProtonVPN, Windscribe
EncPreset string `json:"encryption_preset"` // PIA - needed to get the port number 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) { func (settings *OpenVPNSelection) lines() (lines []string) {
lines = append(lines, lastIndent+"OpenVPN selection:") 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)) lines = append(lines, indent+lastIndent+"Protocol: "+protoToString(settings.TCP))
if settings.CustomPort != 0 { if settings.CustomPort != 0 {

View File

@@ -58,11 +58,9 @@ func (settings *VPN) read(r reader) (err error) {
} }
settings.Type = vpnType settings.Type = vpnType
if !settings.isOpenVPNCustomConfig(r.env) {
if err := settings.Provider.read(r, vpnType); err != nil { if err := settings.Provider.read(r, vpnType); err != nil {
return fmt.Errorf("%w: %s", errReadProviderSettings, err) return fmt.Errorf("%w: %s", errReadProviderSettings, err)
} }
}
switch settings.Type { switch settings.Type {
case constants.OpenVPN: case constants.OpenVPN:
@@ -79,19 +77,3 @@ func (settings *VPN) read(r reader) (err error) {
return nil 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
}

View File

@@ -6,6 +6,9 @@ const (
) )
const ( const (
// Custom is the VPN provider name for custom
// VPN configurations.
Custom = "custom"
// Cyberghost is a VPN provider. // Cyberghost is a VPN provider.
Cyberghost = "cyberghost" Cyberghost = "cyberghost"
// Fastestvpn is a VPN provider. // Fastestvpn is a VPN provider.

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -1,4 +1,4 @@
package custom package extract
import ( import (
"errors" "errors"
@@ -16,23 +16,22 @@ var (
) )
func extractDataFromLines(lines []string) ( func extractDataFromLines(lines []string) (
connection models.Connection, intf string, err error) { connection models.Connection, err error) {
for i, line := range lines { for i, line := range lines {
ip, port, protocol, intfFound, err := extractDataFromLine(line) ip, port, protocol, err := extractDataFromLine(line)
if err != nil { 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) connection.UpdateEmptyWith(ip, port, protocol)
if connection.Protocol != "" && connection.IP != nil && intf != "" { if connection.Protocol != "" && connection.IP != nil {
break break
} }
} }
if connection.IP == nil { if connection.IP == nil {
return connection, "", errRemoteLineNotFound return connection, errRemoteLineNotFound
} }
if connection.Protocol == "" { if connection.Protocol == "" {
@@ -46,41 +45,33 @@ func extractDataFromLines(lines []string) (
} }
} }
return connection, intf, nil return connection, nil
} }
var ( var (
errExtractProto = errors.New("failed extracting protocol from proto line") errExtractProto = errors.New("failed extracting protocol from proto line")
errExtractRemote = errors.New("failed extracting from remote line") errExtractRemote = errors.New("failed extracting from remote line")
errExtractDev = errors.New("failed extracting network interface from dev line")
) )
func extractDataFromLine(line string) ( func extractDataFromLine(line string) (
ip net.IP, port uint16, protocol, intf string, err error) { ip net.IP, port uint16, protocol string, err error) {
switch { switch {
case strings.HasPrefix(line, "proto "): case strings.HasPrefix(line, "proto "):
protocol, err = extractProto(line) protocol, err = extractProto(line)
if err != nil { 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 "): case strings.HasPrefix(line, "remote "):
ip, port, protocol, err = extractRemote(line) ip, port, protocol, err = extractRemote(line)
if err != nil { 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 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 nil, 0, "", "", nil return nil, 0, "", nil
} }
var ( var (
@@ -147,16 +138,3 @@ func extractRemote(line string) (ip net.IP, port uint16,
return ip, port, protocol, nil 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
}

View File

@@ -1,4 +1,4 @@
package custom package extract
import ( import (
"errors" "errors"
@@ -17,7 +17,6 @@ func Test_extractDataFromLines(t *testing.T) {
testCases := map[string]struct { testCases := map[string]struct {
lines []string lines []string
connection models.Connection connection models.Connection
intf string
err error err error
}{ }{
"success": { "success": {
@@ -27,7 +26,6 @@ func Test_extractDataFromLines(t *testing.T) {
Port: 1194, Port: 1194,
Protocol: constants.TCP, Protocol: constants.TCP,
}, },
intf: "tun6",
}, },
"extraction error": { "extraction error": {
lines: []string{"bla bla", "proto bad", "remote 1.2.3.4 1194 tcp"}, 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.Run(name, func(t *testing.T) {
t.Parallel() t.Parallel()
connection, intf, err := extractDataFromLines(testCase.lines) connection, err := extractDataFromLines(testCase.lines)
if testCase.err != nil { if testCase.err != nil {
require.Error(t, err) require.Error(t, err)
@@ -81,7 +79,6 @@ func Test_extractDataFromLines(t *testing.T) {
} }
assert.Equal(t, testCase.connection, connection) 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 ip net.IP
port uint16 port uint16
protocol string protocol string
intf string
isErr error isErr error
}{ }{
"irrelevant line": { "irrelevant line": {
@@ -108,14 +104,6 @@ func Test_extractDataFromLine(t *testing.T) {
line: "proto tcp", line: "proto tcp",
protocol: constants.TCP, protocol: constants.TCP,
}, },
"extract intf error": {
line: "dev ",
isErr: errExtractDev,
},
"extract intf success": {
line: "dev tun3",
intf: "tun3",
},
"extract remote error": { "extract remote error": {
line: "remote bad", line: "remote bad",
isErr: errExtractRemote, isErr: errExtractRemote,
@@ -133,7 +121,7 @@ func Test_extractDataFromLine(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
t.Parallel() t.Parallel()
ip, port, protocol, intf, err := extractDataFromLine(testCase.line) ip, port, protocol, err := extractDataFromLine(testCase.line)
if testCase.isErr != nil { if testCase.isErr != nil {
assert.ErrorIs(t, err, testCase.isErr) 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.ip, ip)
assert.Equal(t, testCase.port, port) assert.Equal(t, testCase.port, port)
assert.Equal(t, testCase.protocol, protocol) 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)
})
}
}

View File

@@ -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)
}

View File

@@ -1,4 +1,4 @@
package custom package extract
import ( import (
"os" "os"

View File

@@ -1,4 +1,4 @@
package custom package extract
import ( import (
"io" "io"

View File

@@ -1,4 +1,4 @@
package custom package extract
import ( import (
"os" "os"

View File

@@ -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)
}

View File

@@ -1,6 +1,8 @@
package custom package custom
import ( import (
"errors"
"fmt"
"strconv" "strconv"
"strings" "strings"
@@ -10,13 +12,27 @@ import (
"github.com/qdm12/gluetun/internal/provider/utils" "github.com/qdm12/gluetun/internal/provider/utils"
) )
func modifyCustomConfig(lines []string, settings configuration.OpenVPN, var ErrExtractData = errors.New("failed extracting information from custom configuration file")
connection models.Connection, intf string) (modified []string) {
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 // Remove some lines
for _, line := range lines { for _, line := range lines {
switch { switch {
case strings.HasPrefix(line, "up "), case
strings.HasPrefix(line, "down "), line == "",
strings.HasPrefix(line, "verb "), strings.HasPrefix(line, "verb "),
strings.HasPrefix(line, "auth-user-pass "), strings.HasPrefix(line, "auth-user-pass "),
strings.HasPrefix(line, "user "), strings.HasPrefix(line, "user "),
@@ -36,7 +52,7 @@ func modifyCustomConfig(lines []string, settings configuration.OpenVPN,
// Add values // Add values
modified = append(modified, connection.OpenVPNProtoLine()) modified = append(modified, connection.OpenVPNProtoLine())
modified = append(modified, connection.OpenVPNRemoteLine()) 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, "mute-replay-warnings")
modified = append(modified, "auth-nocache") modified = append(modified, "auth-nocache")
modified = append(modified, "pull-filter ignore \"auth-token\"") // prevent auth failed loop 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) 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
} }

View File

@@ -10,14 +10,13 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func Test_modifyCustomConfig(t *testing.T) { func Test_modifyConfig(t *testing.T) {
t.Parallel() t.Parallel()
testCases := map[string]struct { testCases := map[string]struct {
lines []string lines []string
settings configuration.OpenVPN settings configuration.OpenVPN
connection models.Connection connection models.Connection
intf string
modified []string modified []string
}{ }{
"mixed": { "mixed": {
@@ -26,6 +25,7 @@ func Test_modifyCustomConfig(t *testing.T) {
"proto tcp", "proto tcp",
"remote 5.5.5.5", "remote 5.5.5.5",
"cipher bla", "cipher bla",
"",
"tun-ipv6", "tun-ipv6",
"keep me here", "keep me here",
"auth bla", "auth bla",
@@ -36,14 +36,15 @@ func Test_modifyCustomConfig(t *testing.T) {
Auth: "auth", Auth: "auth",
MSSFix: 1000, MSSFix: 1000,
ProcUser: "procuser", ProcUser: "procuser",
Interface: "tun3",
}, },
connection: models.Connection{ connection: models.Connection{
IP: net.IPv4(1, 2, 3, 4), IP: net.IPv4(1, 2, 3, 4),
Port: 1194, Port: 1194,
Protocol: constants.UDP, Protocol: constants.UDP,
}, },
intf: "tun3",
modified: []string{ modified: []string{
"up bla",
"keep me here", "keep me here",
"proto udp", "proto udp",
"remote 1.2.3.4 1194", "remote 1.2.3.4 1194",
@@ -62,6 +63,7 @@ func Test_modifyCustomConfig(t *testing.T) {
"pull-filter ignore \"route-ipv6\"", "pull-filter ignore \"route-ipv6\"",
"pull-filter ignore \"ifconfig-ipv6\"", "pull-filter ignore \"ifconfig-ipv6\"",
"user procuser", "user procuser",
"",
}, },
}, },
} }
@@ -71,8 +73,8 @@ func Test_modifyCustomConfig(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
t.Parallel() t.Parallel()
modified := modifyCustomConfig(testCase.lines, modified := modifyConfig(testCase.lines,
testCase.settings, testCase.connection, testCase.intf) testCase.connection, testCase.settings)
assert.Equal(t, testCase.modified, modified) assert.Equal(t, testCase.modified, modified)
}) })

View File

@@ -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),
}
}

View File

@@ -11,7 +11,7 @@ import (
) )
func (c *Cyberghost) BuildConf(connection models.Connection, func (c *Cyberghost) BuildConf(connection models.Connection,
settings configuration.OpenVPN) (lines []string) { settings configuration.OpenVPN) (lines []string, err error) {
if settings.Cipher == "" { if settings.Cipher == "" {
settings.Cipher = constants.AES256cbc settings.Cipher = constants.AES256cbc
} }
@@ -87,5 +87,5 @@ func (c *Cyberghost) BuildConf(connection models.Connection,
lines = append(lines, "") lines = append(lines, "")
return lines return lines, nil
} }

View File

@@ -10,7 +10,7 @@ import (
) )
func (f *Fastestvpn) BuildConf(connection models.Connection, func (f *Fastestvpn) BuildConf(connection models.Connection,
settings configuration.OpenVPN) (lines []string) { settings configuration.OpenVPN) (lines []string, err error) {
if settings.Cipher == "" { if settings.Cipher == "" {
settings.Cipher = constants.AES256cbc settings.Cipher = constants.AES256cbc
} }
@@ -76,5 +76,5 @@ func (f *Fastestvpn) BuildConf(connection models.Connection,
lines = append(lines, "") lines = append(lines, "")
return lines return lines, nil
} }

View File

@@ -10,7 +10,7 @@ import (
) )
func (h *HideMyAss) BuildConf(connection models.Connection, func (h *HideMyAss) BuildConf(connection models.Connection,
settings configuration.OpenVPN) (lines []string) { settings configuration.OpenVPN) (lines []string, err error) {
if settings.Cipher == "" { if settings.Cipher == "" {
settings.Cipher = constants.AES256cbc settings.Cipher = constants.AES256cbc
} }
@@ -75,5 +75,5 @@ func (h *HideMyAss) BuildConf(connection models.Connection,
lines = append(lines, "") lines = append(lines, "")
return lines return lines, nil
} }

View File

@@ -10,7 +10,7 @@ import (
) )
func (i *Ipvanish) BuildConf(connection models.Connection, func (i *Ipvanish) BuildConf(connection models.Connection,
settings configuration.OpenVPN) (lines []string) { settings configuration.OpenVPN) (lines []string, err error) {
if settings.Cipher == "" { if settings.Cipher == "" {
settings.Cipher = constants.AES256cbc settings.Cipher = constants.AES256cbc
} }
@@ -68,5 +68,5 @@ func (i *Ipvanish) BuildConf(connection models.Connection,
lines = append(lines, "") lines = append(lines, "")
return lines return lines, nil
} }

View File

@@ -11,7 +11,7 @@ import (
) )
func (i *Ivpn) BuildConf(connection models.Connection, func (i *Ivpn) BuildConf(connection models.Connection,
settings configuration.OpenVPN) (lines []string) { settings configuration.OpenVPN) (lines []string, err error) {
if settings.Cipher == "" { if settings.Cipher == "" {
settings.Cipher = constants.AES256cbc settings.Cipher = constants.AES256cbc
} }
@@ -77,5 +77,5 @@ func (i *Ivpn) BuildConf(connection models.Connection,
lines = append(lines, "") lines = append(lines, "")
return lines return lines, nil
} }

View File

@@ -10,7 +10,7 @@ import (
) )
func (m *Mullvad) BuildConf(connection models.Connection, func (m *Mullvad) BuildConf(connection models.Connection,
settings configuration.OpenVPN) (lines []string) { settings configuration.OpenVPN) (lines []string, err error) {
if settings.Cipher == "" { if settings.Cipher == "" {
settings.Cipher = constants.AES256cbc settings.Cipher = constants.AES256cbc
} }
@@ -83,5 +83,5 @@ func (m *Mullvad) BuildConf(connection models.Connection,
lines = append(lines, "") lines = append(lines, "")
return lines return lines, nil
} }

View File

@@ -10,7 +10,7 @@ import (
) )
func (n *Nordvpn) BuildConf(connection models.Connection, func (n *Nordvpn) BuildConf(connection models.Connection,
settings configuration.OpenVPN) (lines []string) { settings configuration.OpenVPN) (lines []string, err error) {
if settings.Cipher == "" { if settings.Cipher == "" {
settings.Cipher = constants.AES256cbc settings.Cipher = constants.AES256cbc
} }
@@ -81,5 +81,5 @@ func (n *Nordvpn) BuildConf(connection models.Connection,
lines = append(lines, "") lines = append(lines, "")
return lines return lines, nil
} }

View File

@@ -10,7 +10,7 @@ import (
) )
func (p *Privado) BuildConf(connection models.Connection, func (p *Privado) BuildConf(connection models.Connection,
settings configuration.OpenVPN) (lines []string) { settings configuration.OpenVPN) (lines []string, err error) {
if settings.Cipher == "" { if settings.Cipher == "" {
settings.Cipher = constants.AES256cbc settings.Cipher = constants.AES256cbc
} }
@@ -70,5 +70,5 @@ func (p *Privado) BuildConf(connection models.Connection,
lines = append(lines, "") lines = append(lines, "")
return lines return lines, nil
} }

View File

@@ -10,7 +10,7 @@ import (
) )
func (p *PIA) BuildConf(connection models.Connection, 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 var defaultCipher, defaultAuth, X509CRL, certificate string
switch settings.EncPreset { switch settings.EncPreset {
case constants.PIAEncryptionPresetNormal: case constants.PIAEncryptionPresetNormal:
@@ -93,5 +93,5 @@ func (p *PIA) BuildConf(connection models.Connection,
lines = append(lines, "") lines = append(lines, "")
return lines return lines, nil
} }

View File

@@ -10,7 +10,7 @@ import (
) )
func (p *Privatevpn) BuildConf(connection models.Connection, func (p *Privatevpn) BuildConf(connection models.Connection,
settings configuration.OpenVPN) (lines []string) { settings configuration.OpenVPN) (lines []string, err error) {
if settings.Cipher == "" { if settings.Cipher == "" {
settings.Cipher = constants.AES128gcm settings.Cipher = constants.AES128gcm
} }
@@ -73,5 +73,5 @@ func (p *Privatevpn) BuildConf(connection models.Connection,
lines = append(lines, "") lines = append(lines, "")
return lines return lines, nil
} }

View File

@@ -10,7 +10,7 @@ import (
) )
func (p *Protonvpn) BuildConf(connection models.Connection, func (p *Protonvpn) BuildConf(connection models.Connection,
settings configuration.OpenVPN) (lines []string) { settings configuration.OpenVPN) (lines []string, err error) {
if settings.Cipher == "" { if settings.Cipher == "" {
settings.Cipher = constants.AES256cbc settings.Cipher = constants.AES256cbc
} }
@@ -80,5 +80,5 @@ func (p *Protonvpn) BuildConf(connection models.Connection,
lines = append(lines, "") lines = append(lines, "")
return lines return lines, nil
} }

View File

@@ -11,6 +11,7 @@ import (
"github.com/qdm12/gluetun/internal/configuration" "github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models" "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/cyberghost"
"github.com/qdm12/gluetun/internal/provider/fastestvpn" "github.com/qdm12/gluetun/internal/provider/fastestvpn"
"github.com/qdm12/gluetun/internal/provider/hidemyass" "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. // Provider contains methods to read and modify the openvpn configuration to connect as a client.
type Provider interface { type Provider interface {
GetConnection(selection configuration.ServerSelection) (connection models.Connection, err error) 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 PortForwarder
} }
@@ -50,6 +51,8 @@ type PortForwarder interface {
func New(provider string, allServers models.AllServers, timeNow func() time.Time) Provider { func New(provider string, allServers models.AllServers, timeNow func() time.Time) Provider {
randSource := rand.NewSource(timeNow().UnixNano()) randSource := rand.NewSource(timeNow().UnixNano())
switch provider { switch provider {
case constants.Custom:
return custom.New()
case constants.Cyberghost: case constants.Cyberghost:
return cyberghost.New(allServers.Cyberghost.Servers, randSource) return cyberghost.New(allServers.Cyberghost.Servers, randSource)
case constants.Fastestvpn: case constants.Fastestvpn:

View File

@@ -10,7 +10,7 @@ import (
) )
func (p *Purevpn) BuildConf(connection models.Connection, func (p *Purevpn) BuildConf(connection models.Connection,
settings configuration.OpenVPN) (lines []string) { settings configuration.OpenVPN) (lines []string, err error) {
if settings.Cipher == "" { if settings.Cipher == "" {
settings.Cipher = constants.AES256gcm settings.Cipher = constants.AES256gcm
} }
@@ -84,5 +84,5 @@ func (p *Purevpn) BuildConf(connection models.Connection,
lines = append(lines, "") lines = append(lines, "")
return lines return lines, nil
} }

View File

@@ -10,7 +10,7 @@ import (
) )
func (s *Surfshark) BuildConf(connection models.Connection, func (s *Surfshark) BuildConf(connection models.Connection,
settings configuration.OpenVPN) (lines []string) { settings configuration.OpenVPN) (lines []string, err error) {
if settings.Cipher == "" { if settings.Cipher == "" {
settings.Cipher = constants.AES256gcm settings.Cipher = constants.AES256gcm
} }
@@ -78,5 +78,5 @@ func (s *Surfshark) BuildConf(connection models.Connection,
lines = append(lines, "") lines = append(lines, "")
return lines return lines, nil
} }

View File

@@ -10,7 +10,7 @@ import (
) )
func (t *Torguard) BuildConf(connection models.Connection, func (t *Torguard) BuildConf(connection models.Connection,
settings configuration.OpenVPN) (lines []string) { settings configuration.OpenVPN) (lines []string, err error) {
if settings.Cipher == "" { if settings.Cipher == "" {
settings.Cipher = constants.AES256gcm settings.Cipher = constants.AES256gcm
} }
@@ -84,5 +84,5 @@ func (t *Torguard) BuildConf(connection models.Connection,
lines = append(lines, "") lines = append(lines, "")
return lines return lines, nil
} }

View File

@@ -10,7 +10,7 @@ import (
) )
func (p *Provider) BuildConf(connection models.Connection, func (p *Provider) BuildConf(connection models.Connection,
settings configuration.OpenVPN) (lines []string) { settings configuration.OpenVPN) (lines []string, err error) {
lines = []string{ lines = []string{
"client", "client",
"dev " + settings.Interface, "dev " + settings.Interface,
@@ -71,5 +71,5 @@ func (p *Provider) BuildConf(connection models.Connection,
lines = append(lines, "") lines = append(lines, "")
return lines return lines, nil
} }

View File

@@ -10,7 +10,7 @@ import (
) )
func (v *Vyprvpn) BuildConf(connection models.Connection, func (v *Vyprvpn) BuildConf(connection models.Connection,
settings configuration.OpenVPN) (lines []string) { settings configuration.OpenVPN) (lines []string, err error) {
if settings.Cipher == "" { if settings.Cipher == "" {
settings.Cipher = constants.AES256cbc settings.Cipher = constants.AES256cbc
} }
@@ -65,5 +65,5 @@ func (v *Vyprvpn) BuildConf(connection models.Connection,
lines = append(lines, "") lines = append(lines, "")
return lines return lines, nil
} }

View File

@@ -11,7 +11,7 @@ import (
) )
func (w *Windscribe) BuildConf(connection models.Connection, func (w *Windscribe) BuildConf(connection models.Connection,
settings configuration.OpenVPN) (lines []string) { settings configuration.OpenVPN) (lines []string, err error) {
if settings.Cipher == "" { if settings.Cipher == "" {
settings.Cipher = constants.AES256cbc settings.Cipher = constants.AES256cbc
} }
@@ -81,5 +81,5 @@ func (w *Windscribe) BuildConf(connection models.Connection,
lines = append(lines, "") lines = append(lines, "")
return lines return lines, nil
} }

View File

@@ -7,15 +7,14 @@ import (
"github.com/qdm12/gluetun/internal/configuration" "github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/firewall" "github.com/qdm12/gluetun/internal/firewall"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/openvpn" "github.com/qdm12/gluetun/internal/openvpn"
"github.com/qdm12/gluetun/internal/openvpn/custom"
"github.com/qdm12/gluetun/internal/provider" "github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/golibs/command" "github.com/qdm12/golibs/command"
"github.com/qdm12/golibs/logging" "github.com/qdm12/golibs/logging"
) )
var ( var (
errServerConn = errors.New("failed finding a valid server connection")
errBuildConfig = errors.New("failed building configuration") errBuildConfig = errors.New("failed building configuration")
errWriteConfig = errors.New("failed writing configuration to file") errWriteConfig = errors.New("failed writing configuration to file")
errWriteAuth = errors.New("failed writing auth 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, openvpnConf openvpn.Interface, providerConf provider.Provider,
settings configuration.VPN, starter command.Starter, logger logging.Logger) ( settings configuration.VPN, starter command.Starter, logger logging.Logger) (
runner vpnRunner, serverName string, err error) { runner vpnRunner, serverName string, err error) {
var connection models.Connection connection, err := providerConf.GetConnection(settings.Provider.ServerSelection)
var netInterface string if err != nil {
var lines []string return nil, "", fmt.Errorf("%w: %s", errServerConn, err)
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)
} }
lines, err := providerConf.BuildConf(connection, settings.OpenVPN)
if err != nil { if err != nil {
return nil, "", fmt.Errorf("%w: %s", errBuildConfig, err) 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) return nil, "", fmt.Errorf("%w: %s", errFirewall, err)
} }

View File

@@ -28,6 +28,7 @@
- Gluetun entire logs available at control server, maybe in structured format - Gluetun entire logs available at control server, maybe in structured format
- Authentication with the control server - Authentication with the control server
- Get announcement from Github file - Get announcement from Github file
- Support multiple connections in custom ovpn
## Gluetun V4 ## Gluetun V4
@@ -47,4 +48,7 @@
- Change `VPNSP` to `VPN_SERVICE_PROVIDER` - Change `VPNSP` to `VPN_SERVICE_PROVIDER`
- Change `REGION` (etc.) to `SERVER_REGIONS` - Change `REGION` (etc.) to `SERVER_REGIONS`
- Remove `PUBLICIP_FILE` - 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 - Split servers.json and compress it
- Use relative paths everywhere instead of absolute