3
.github/ISSUE_TEMPLATE/provider.md
vendored
3
.github/ISSUE_TEMPLATE/provider.md
vendored
@@ -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.
|
||||
|
||||
@@ -69,6 +69,7 @@ ENV VPNSP=pia \
|
||||
OPENVPN_ROOT=yes \
|
||||
OPENVPN_TARGET_IP= \
|
||||
OPENVPN_IPV6=off \
|
||||
OPENVPN_CUSTOM_CONFIG= \
|
||||
TZ= \
|
||||
PUID= \
|
||||
PGID= \
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
205
internal/openvpn/custom.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
if err != nil {
|
||||
l.logger.Error(err)
|
||||
l.signalCrashedStatus()
|
||||
l.cancel()
|
||||
return
|
||||
|
||||
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)
|
||||
} else {
|
||||
lines, connection, err = l.processCustomConfig(settings)
|
||||
if err != nil {
|
||||
l.signalCrashedStatus()
|
||||
l.logAndWait(ctx, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
lines := providerConf.BuildConf(connection, l.username, settings)
|
||||
|
||||
if err := writeOpenvpnConf(lines, l.openFile); err != nil {
|
||||
l.logger.Error(err)
|
||||
|
||||
Reference in New Issue
Block a user