Feature: custom Openvpn configuration file, fixes #223 (#402)

This commit is contained in:
Quentin McGaw
2021-03-13 08:51:05 -05:00
committed by GitHub
parent aca112fa42
commit fa220f9e93
6 changed files with 245 additions and 12 deletions

View File

@@ -13,4 +13,5 @@ One of the following is required:
- Publicly accessible URL to the list of servers **and attach** an example Openvpn configuration file for both TCP and UDP
If the list of servers requires to login **or** is hidden behind an interactive configurator,
it's not possible to support the provider yet. Please instead subscribe to issue #223 which would solve it.
it's not possible to support the provider yet.
[The Wiki](https://github.com/qdm12/gluetun/wiki/Openvpn-file) describes how to use an Openvpn configuration file.

View File

@@ -69,6 +69,7 @@ ENV VPNSP=pia \
OPENVPN_ROOT=yes \
OPENVPN_TARGET_IP= \
OPENVPN_IPV6=off \
OPENVPN_CUSTOM_CONFIG= \
TZ= \
PUID= \
PGID= \

View File

@@ -20,6 +20,7 @@ type OpenVPN struct {
Cipher string `json:"cipher"`
Auth string `json:"auth"`
Provider Provider `json:"provider"`
Config string `json:"custom_config"`
}
func (settings *OpenVPN) String() string {
@@ -42,6 +43,10 @@ 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)
}
lines = append(lines, indent+lastIndent+"Provider:")
for _, line := range settings.Provider.lines() {
lines = append(lines, indent+indent+line)
@@ -69,7 +74,14 @@ func (settings *OpenVPN) read(r reader) (err error) {
settings.Provider.Name = vpnsp
settings.User, err = r.getFromEnvOrSecretFile("OPENVPN_USER", true, []string{"USER"})
settings.Config, err = r.env.Get("OPENVPN_CUSTOM_CONFIG", params.CaseSensitiveValue())
if err != nil {
return err
}
credentialsRequired := len(settings.Config) == 0
settings.User, err = r.getFromEnvOrSecretFile("OPENVPN_USER", credentialsRequired, []string{"USER"})
if err != nil {
return err
}
@@ -79,7 +91,7 @@ func (settings *OpenVPN) read(r reader) (err error) {
if settings.Provider.Name == constants.Mullvad {
settings.Password = "m"
} else {
settings.Password, err = r.getFromEnvOrSecretFile("OPENVPN_PASSWORD", true, []string{"PASSWORD"})
settings.Password, err = r.getFromEnvOrSecretFile("OPENVPN_PASSWORD", credentialsRequired, []string{"PASSWORD"})
if err != nil {
return err
}

View File

@@ -19,7 +19,7 @@ func Test_OpenVPN_JSON(t *testing.T) {
data, err := json.Marshal(in)
require.NoError(t, err)
//nolint:lll
assert.Equal(t, `{"user":"","password":"","verbosity":0,"mssfix":0,"run_as_root":true,"cipher":"","auth":"","provider":{"name":"name","server_selection":{"network_protocol":"","regions":null,"group":"","countries":null,"cities":null,"hostnames":null,"isps":null,"owned":false,"custom_port":0,"numbers":null,"encryption_preset":""},"extra_config":{"encryption_preset":"","openvpn_ipv6":false},"port_forwarding":{"enabled":false,"filepath":""}}}`, string(data))
assert.Equal(t, `{"user":"","password":"","verbosity":0,"mssfix":0,"run_as_root":true,"cipher":"","auth":"","provider":{"name":"name","server_selection":{"network_protocol":"","regions":null,"group":"","countries":null,"cities":null,"hostnames":null,"isps":null,"owned":false,"custom_port":0,"numbers":null,"encryption_preset":""},"extra_config":{"encryption_preset":"","openvpn_ipv6":false},"port_forwarding":{"enabled":false,"filepath":""}},"custom_config":""}`, string(data))
var out OpenVPN
err = json.Unmarshal(data, &out)
require.NoError(t, err)

205
internal/openvpn/custom.go Normal file
View File

@@ -0,0 +1,205 @@
package openvpn
import (
"errors"
"fmt"
"io"
"net"
"strconv"
"strings"
"github.com/qdm12/gluetun/internal/configuration"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/golibs/os"
)
var errProcessCustomConfig = errors.New("cannot process custom config")
func (l *looper) processCustomConfig(settings configuration.OpenVPN) (
lines []string, connection models.OpenVPNConnection, err error) {
lines, err = readCustomConfigLines(settings.Config, l.openFile)
if err != nil {
return nil, connection, fmt.Errorf("%w: %s", errProcessCustomConfig, err)
}
lines = modifyCustomConfig(lines, l.username, settings)
connection, err = extractConnectionFromLines(lines)
if err != nil {
return nil, connection, fmt.Errorf("%w: %s", errProcessCustomConfig, err)
}
lines = setConnectionToLines(lines, connection)
return lines, connection, nil
}
func readCustomConfigLines(filepath string, openFile os.OpenFileFunc) (
lines []string, err error) {
file, err := openFile(filepath, os.O_RDONLY, 0)
if err != nil {
return nil, err
}
defer file.Close()
b, err := io.ReadAll(file)
if err != nil {
return nil, err
}
if err := file.Close(); err != nil {
return nil, err
}
return strings.Split(string(b), "\n"), nil
}
func modifyCustomConfig(lines []string, username string,
settings configuration.OpenVPN) (modified []string) {
// Remove some lines
for _, line := range lines {
switch {
case strings.HasPrefix(line, "up "),
strings.HasPrefix(line, "down "),
strings.HasPrefix(line, "verb "),
strings.HasPrefix(line, "auth-user-pass "),
len(settings.Cipher) > 0 && strings.HasPrefix(line, "cipher "),
len(settings.Auth) > 0 && strings.HasPrefix(line, "auth "),
settings.MSSFix > 0 && strings.HasPrefix(line, "mssfix "),
!settings.Provider.ExtraConfigOptions.OpenVPNIPv6 && strings.HasPrefix(line, "tun-ipv6"):
default:
modified = append(modified, line)
}
}
// Add values
modified = append(modified, "mute-replay-warnings")
modified = append(modified, "auth-nocache")
modified = append(modified, "pull-filter ignore \"auth-token\"") // prevent auth failed loop
modified = append(modified, `pull-filter ignore "ping-restart"`)
modified = append(modified, "auth-retry nointeract")
modified = append(modified, "suppress-timestamps")
modified = append(modified, "auth-user-pass "+constants.OpenVPNAuthConf)
modified = append(modified, "verb "+strconv.Itoa(settings.Verbosity))
if len(settings.Cipher) > 0 {
modified = append(modified, "cipher "+settings.Cipher)
}
if len(settings.Auth) > 0 {
modified = append(modified, "auth "+settings.Auth)
}
if settings.MSSFix > 0 {
modified = append(modified, "mssfix "+strconv.Itoa(int(settings.MSSFix)))
}
if !settings.Provider.ExtraConfigOptions.OpenVPNIPv6 {
modified = append(modified, `pull-filter ignore "route-ipv6"`)
modified = append(modified, `pull-filter ignore "ifconfig-ipv6"`)
}
if !settings.Root {
modified = append(modified, "user "+username)
}
return modified
}
var errExtractConnection = errors.New("cannot extract connection")
// extractConnectionFromLines always takes the first remote line only.
func extractConnectionFromLines(lines []string) ( //nolint:gocognit
connection models.OpenVPNConnection, err error) {
for _, line := range lines {
switch {
case strings.HasPrefix(line, "proto "):
fields := strings.Fields(line)
if n := len(fields); n != 2 { //nolint:gomnd
return connection, fmt.Errorf(
"%w: proto line has %d fields instead of 2: %s",
errExtractConnection, n, line)
}
connection.Protocol = fields[1]
// only take the first remote line
case strings.HasPrefix(line, "remote ") && connection.IP == nil:
fields := strings.Fields(line)
n := len(fields)
//nolint:gomnd
if n < 2 {
return connection, fmt.Errorf(
"%w: remote line has not enough fields: %s",
errExtractConnection, line)
}
host := fields[1]
if ip := net.ParseIP(host); ip != nil {
connection.IP = ip
} else {
return connection, fmt.Errorf(
"%w: for now, the remote line must contain an IP adddress: %s",
errExtractConnection, line)
// TODO resolve hostname once there is an option to allow it through
// the firewall before the VPN is up.
}
if n > 2 { //nolint:gomnd
port, err := strconv.Atoi(fields[2])
if err != nil {
return connection, fmt.Errorf(
"%w: remote line has an invalid port: %s",
errExtractConnection, line)
}
connection.Port = uint16(port)
}
if n > 3 { //nolint:gomnd
connection.Protocol = strings.ToLower(fields[3])
}
if n > 4 { //nolint:gomnd
return connection, fmt.Errorf(
"%w: remote line has too many fields: %s",
errExtractConnection, line)
}
}
if connection.Protocol != "" && connection.IP != nil {
break
}
}
if connection.IP == nil {
return connection, fmt.Errorf("%w: remote line not found", errExtractConnection)
}
switch connection.Protocol {
case "":
return connection, fmt.Errorf("%w: network protocol not found", errExtractConnection)
case "tcp", "udp":
default:
return connection, fmt.Errorf("%w: network protocol not supported: %s", errExtractConnection, connection.Protocol)
}
if connection.Port == 0 {
if connection.Protocol == "tcp" {
const defaultPort uint16 = 443
connection.Port = defaultPort
} else {
const defaultPort uint16 = 1194
connection.Port = defaultPort
}
}
return connection, nil
}
func setConnectionToLines(lines []string, connection models.OpenVPNConnection) (modified []string) {
for i, line := range lines {
switch {
case strings.HasPrefix(line, "proto "):
lines[i] = "proto " + connection.Protocol
case strings.HasPrefix(line, "remote "):
lines[i] = "remote " + connection.IP.String() + " " + strconv.Itoa(int(connection.Port))
}
}
return lines
}

View File

@@ -99,7 +99,7 @@ func (l *looper) signalCrashedStatus() {
}
}
func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) { //nolint:gocognit
defer wg.Done()
select {
case <-l.start:
@@ -110,15 +110,29 @@ func (l *looper) Run(ctx context.Context, wg *sync.WaitGroup) {
for ctx.Err() == nil {
settings, allServers := l.state.getSettingsAndServers()
providerConf := provider.New(settings.Provider.Name, allServers, time.Now)
connection, err := providerConf.GetOpenVPNConnection(settings.Provider.ServerSelection)
var connection models.OpenVPNConnection
var lines []string
var err error
if len(settings.Config) == 0 {
connection, err = providerConf.GetOpenVPNConnection(settings.Provider.ServerSelection)
if err != nil {
l.logger.Error(err)
l.signalCrashedStatus()
l.cancel()
return
}
lines := providerConf.BuildConf(connection, l.username, settings)
lines = providerConf.BuildConf(connection, l.username, settings)
} else {
lines, connection, err = l.processCustomConfig(settings)
if err != nil {
l.signalCrashedStatus()
l.logAndWait(ctx, err)
continue
}
}
if err := writeOpenvpnConf(lines, l.openFile); err != nil {
l.logger.Error(err)