Files
gluetun/internal/openvpn/custom.go
2021-03-15 02:13:10 +00:00

206 lines
5.7 KiB
Go

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 "":
connection.Protocol = "udp"
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
}