chore(settings): refactor settings processing (#756)
- Better settings tree structure logged using `qdm12/gotree` - Read settings from environment variables, then files, then secret files - Settings methods to default them, merge them and override them - `DNS_PLAINTEXT_ADDRESS` default changed to `127.0.0.1` to use DoT. Warning added if set to something else. - `HTTPPROXY_LISTENING_ADDRESS` instead of `HTTPPROXY_PORT` (with retro-compatibility)
This commit is contained in:
82
internal/configuration/settings/dns.go
Normal file
82
internal/configuration/settings/dns.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
// DNS contains settings to configure DNS.
|
||||
type DNS struct {
|
||||
// ServerAddress is the DNS server to use inside
|
||||
// the Go program and for the system.
|
||||
// It defaults to '127.0.0.1' to be used with the
|
||||
// DoT server. It cannot be nil in the internal
|
||||
// state.
|
||||
ServerAddress net.IP
|
||||
// KeepNameserver is true if the Docker DNS server
|
||||
// found in /etc/resolv.conf should be kept.
|
||||
// Note settings this to true will go around the
|
||||
// DoT server blocking.
|
||||
// It defaults to false and cannot be nil in the
|
||||
// internal state.
|
||||
KeepNameserver *bool
|
||||
// DOT contains settings to configure the DoT
|
||||
// server.
|
||||
DoT DoT
|
||||
}
|
||||
|
||||
func (d DNS) validate() (err error) {
|
||||
err = d.DoT.validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed validating DoT settings: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DNS) Copy() (copied DNS) {
|
||||
return DNS{
|
||||
ServerAddress: helpers.CopyIP(d.ServerAddress),
|
||||
KeepNameserver: helpers.CopyBoolPtr(d.KeepNameserver),
|
||||
DoT: d.DoT.copy(),
|
||||
}
|
||||
}
|
||||
|
||||
// mergeWith merges the other settings into any
|
||||
// unset field of the receiver settings object.
|
||||
func (d *DNS) mergeWith(other DNS) {
|
||||
d.ServerAddress = helpers.MergeWithIP(d.ServerAddress, other.ServerAddress)
|
||||
d.KeepNameserver = helpers.MergeWithBool(d.KeepNameserver, other.KeepNameserver)
|
||||
d.DoT.mergeWith(other.DoT)
|
||||
}
|
||||
|
||||
// overrideWith overrides fields of the receiver
|
||||
// settings object with any field set in the other
|
||||
// settings.
|
||||
func (d *DNS) overrideWith(other DNS) {
|
||||
d.ServerAddress = helpers.OverrideWithIP(d.ServerAddress, other.ServerAddress)
|
||||
d.KeepNameserver = helpers.OverrideWithBool(d.KeepNameserver, other.KeepNameserver)
|
||||
d.DoT.overrideWith(other.DoT)
|
||||
}
|
||||
|
||||
func (d *DNS) setDefaults() {
|
||||
localhost := net.IPv4(127, 0, 0, 1) //nolint:gomnd
|
||||
d.ServerAddress = helpers.DefaultIP(d.ServerAddress, localhost)
|
||||
d.KeepNameserver = helpers.DefaultBool(d.KeepNameserver, false)
|
||||
d.DoT.setDefaults()
|
||||
}
|
||||
|
||||
func (d DNS) String() string {
|
||||
return d.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (d DNS) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("DNS settings:")
|
||||
node.Appendf("DNS server address to use: %s", d.ServerAddress)
|
||||
node.Appendf("Keep existing nameserver(s): %s", helpers.BoolPtrToYesNo(d.KeepNameserver))
|
||||
node.AppendNode(d.DoT.toLinesNode())
|
||||
return node
|
||||
}
|
||||
138
internal/configuration/settings/dnsblacklist.go
Normal file
138
internal/configuration/settings/dnsblacklist.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/qdm12/dns/pkg/blacklist"
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gotree"
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
// DNSBlacklist is settings for the DNS blacklist building.
|
||||
type DNSBlacklist struct {
|
||||
BlockMalicious *bool
|
||||
BlockAds *bool
|
||||
BlockSurveillance *bool
|
||||
AllowedHosts []string
|
||||
AddBlockedHosts []string
|
||||
AddBlockedIPs []netaddr.IP
|
||||
AddBlockedIPPrefixes []netaddr.IPPrefix
|
||||
}
|
||||
|
||||
func (b *DNSBlacklist) setDefaults() {
|
||||
b.BlockMalicious = helpers.DefaultBool(b.BlockMalicious, true)
|
||||
b.BlockAds = helpers.DefaultBool(b.BlockAds, false)
|
||||
b.BlockSurveillance = helpers.DefaultBool(b.BlockSurveillance, true)
|
||||
}
|
||||
|
||||
var hostRegex = regexp.MustCompile(`^([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9_])(\.([a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9]))*$`) //nolint:lll
|
||||
|
||||
var (
|
||||
ErrAllowedHostNotValid = errors.New("allowed host is not valid")
|
||||
ErrBlockedHostNotValid = errors.New("blocked host is not valid")
|
||||
)
|
||||
|
||||
func (b DNSBlacklist) validate() (err error) {
|
||||
for _, host := range b.AllowedHosts {
|
||||
if !hostRegex.MatchString(host) {
|
||||
return fmt.Errorf("%w: %s", ErrAllowedHostNotValid, host)
|
||||
}
|
||||
}
|
||||
|
||||
for _, host := range b.AddBlockedHosts {
|
||||
if !hostRegex.MatchString(host) {
|
||||
return fmt.Errorf("%w: %s", ErrBlockedHostNotValid, host)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b DNSBlacklist) copy() (copied DNSBlacklist) {
|
||||
return DNSBlacklist{
|
||||
BlockMalicious: helpers.CopyBoolPtr(b.BlockMalicious),
|
||||
BlockAds: helpers.CopyBoolPtr(b.BlockAds),
|
||||
BlockSurveillance: helpers.CopyBoolPtr(b.BlockSurveillance),
|
||||
AllowedHosts: helpers.CopyStringSlice(b.AllowedHosts),
|
||||
AddBlockedHosts: helpers.CopyStringSlice(b.AddBlockedHosts),
|
||||
AddBlockedIPs: helpers.CopyNetaddrIPsSlice(b.AddBlockedIPs),
|
||||
AddBlockedIPPrefixes: helpers.CopyIPPrefixSlice(b.AddBlockedIPPrefixes),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *DNSBlacklist) mergeWith(other DNSBlacklist) {
|
||||
b.BlockMalicious = helpers.MergeWithBool(b.BlockMalicious, other.BlockMalicious)
|
||||
b.BlockAds = helpers.MergeWithBool(b.BlockAds, other.BlockAds)
|
||||
b.BlockSurveillance = helpers.MergeWithBool(b.BlockSurveillance, other.BlockSurveillance)
|
||||
b.AllowedHosts = helpers.MergeStringSlices(b.AllowedHosts, other.AllowedHosts)
|
||||
b.AddBlockedHosts = helpers.MergeStringSlices(b.AddBlockedHosts, other.AddBlockedHosts)
|
||||
b.AddBlockedIPs = helpers.MergeNetaddrIPsSlices(b.AddBlockedIPs, other.AddBlockedIPs)
|
||||
b.AddBlockedIPPrefixes = helpers.MergeIPPrefixesSlices(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes)
|
||||
}
|
||||
|
||||
func (b *DNSBlacklist) overrideWith(other DNSBlacklist) {
|
||||
b.BlockMalicious = helpers.OverrideWithBool(b.BlockMalicious, other.BlockMalicious)
|
||||
b.BlockAds = helpers.OverrideWithBool(b.BlockAds, other.BlockAds)
|
||||
b.BlockSurveillance = helpers.OverrideWithBool(b.BlockSurveillance, other.BlockSurveillance)
|
||||
b.AllowedHosts = helpers.OverrideWithStringSlice(b.AllowedHosts, other.AllowedHosts)
|
||||
b.AddBlockedHosts = helpers.OverrideWithStringSlice(b.AddBlockedHosts, other.AddBlockedHosts)
|
||||
b.AddBlockedIPs = helpers.OverrideWithNetaddrIPsSlice(b.AddBlockedIPs, other.AddBlockedIPs)
|
||||
b.AddBlockedIPPrefixes = helpers.OverrideWithIPPrefixesSlice(b.AddBlockedIPPrefixes, other.AddBlockedIPPrefixes)
|
||||
}
|
||||
|
||||
func (b DNSBlacklist) ToBlacklistFormat() (settings blacklist.BuilderSettings, err error) {
|
||||
return blacklist.BuilderSettings{
|
||||
BlockMalicious: *b.BlockMalicious,
|
||||
BlockAds: *b.BlockAds,
|
||||
BlockSurveillance: *b.BlockSurveillance,
|
||||
AllowedHosts: b.AllowedHosts,
|
||||
AddBlockedHosts: b.AddBlockedHosts,
|
||||
AddBlockedIPs: b.AddBlockedIPs,
|
||||
AddBlockedIPPrefixes: b.AddBlockedIPPrefixes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b DNSBlacklist) String() string {
|
||||
return b.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (b DNSBlacklist) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("DNS filtering settings:")
|
||||
|
||||
node.Appendf("Block malicious: %s", helpers.BoolPtrToYesNo(b.BlockMalicious))
|
||||
node.Appendf("Block ads: %s", helpers.BoolPtrToYesNo(b.BlockAds))
|
||||
node.Appendf("Block surveillance: %s", helpers.BoolPtrToYesNo(b.BlockSurveillance))
|
||||
|
||||
if len(b.AllowedHosts) > 0 {
|
||||
allowedHostsNode := node.Appendf("Allowed hosts:")
|
||||
for _, host := range b.AllowedHosts {
|
||||
allowedHostsNode.Appendf(host)
|
||||
}
|
||||
}
|
||||
|
||||
if len(b.AddBlockedHosts) > 0 {
|
||||
blockedHostsNode := node.Appendf("Blocked hosts:")
|
||||
for _, host := range b.AddBlockedHosts {
|
||||
blockedHostsNode.Appendf(host)
|
||||
}
|
||||
}
|
||||
|
||||
if len(b.AddBlockedIPs) > 0 {
|
||||
blockedIPsNode := node.Appendf("Blocked IP addresses:")
|
||||
for _, ip := range b.AddBlockedIPs {
|
||||
blockedIPsNode.Appendf(ip.String())
|
||||
}
|
||||
}
|
||||
|
||||
if len(b.AddBlockedIPPrefixes) > 0 {
|
||||
blockedIPPrefixesNode := node.Appendf("Blocked IP networks:")
|
||||
for _, ipNetwork := range b.AddBlockedIPPrefixes {
|
||||
blockedIPPrefixesNode.Appendf(ipNetwork.String())
|
||||
}
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
113
internal/configuration/settings/dot.go
Normal file
113
internal/configuration/settings/dot.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
// DoT contains settings to configure the DoT server.
|
||||
type DoT struct {
|
||||
// Enabled is true if the DoT server should be running
|
||||
// and used. It defaults to true, and cannot be nil
|
||||
// in the internal state.
|
||||
Enabled *bool
|
||||
// UpdatePeriod is the period to update DNS block
|
||||
// lists and cryptographic files for DNSSEC validation.
|
||||
// It can be set to 0 to disable the update.
|
||||
// It defaults to 24h and cannot be nil in
|
||||
// the internal state.
|
||||
UpdatePeriod *time.Duration
|
||||
// Unbound contains settings to configure Unbound.
|
||||
Unbound Unbound
|
||||
// Blacklist contains settings to configure the filter
|
||||
// block lists.
|
||||
Blacklist DNSBlacklist
|
||||
}
|
||||
|
||||
var (
|
||||
ErrDoTUpdatePeriodTooShort = errors.New("update period is too short")
|
||||
)
|
||||
|
||||
func (d DoT) validate() (err error) {
|
||||
const minUpdatePeriod = 30 * time.Second
|
||||
if *d.UpdatePeriod < minUpdatePeriod {
|
||||
return fmt.Errorf("%w: %s must be bigger than %s",
|
||||
ErrDoTUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod)
|
||||
}
|
||||
|
||||
err = d.Unbound.validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = d.Blacklist.validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DoT) copy() (copied DoT) {
|
||||
return DoT{
|
||||
Enabled: helpers.CopyBoolPtr(d.Enabled),
|
||||
UpdatePeriod: helpers.CopyDurationPtr(d.UpdatePeriod),
|
||||
Unbound: d.Unbound.copy(),
|
||||
Blacklist: d.Blacklist.copy(),
|
||||
}
|
||||
}
|
||||
|
||||
// mergeWith merges the other settings into any
|
||||
// unset field of the receiver settings object.
|
||||
func (d *DoT) mergeWith(other DoT) {
|
||||
d.Enabled = helpers.MergeWithBool(d.Enabled, other.Enabled)
|
||||
d.UpdatePeriod = helpers.MergeWithDuration(d.UpdatePeriod, other.UpdatePeriod)
|
||||
d.Unbound.mergeWith(other.Unbound)
|
||||
d.Blacklist.mergeWith(other.Blacklist)
|
||||
}
|
||||
|
||||
// overrideWith overrides fields of the receiver
|
||||
// settings object with any field set in the other
|
||||
// settings.
|
||||
func (d *DoT) overrideWith(other DoT) {
|
||||
d.Enabled = helpers.OverrideWithBool(d.Enabled, other.Enabled)
|
||||
d.UpdatePeriod = helpers.OverrideWithDuration(d.UpdatePeriod, other.UpdatePeriod)
|
||||
d.Unbound.overrideWith(other.Unbound)
|
||||
d.Blacklist.overrideWith(other.Blacklist)
|
||||
}
|
||||
|
||||
func (d *DoT) setDefaults() {
|
||||
d.Enabled = helpers.DefaultBool(d.Enabled, true)
|
||||
const defaultUpdatePeriod = 24 * time.Hour
|
||||
d.UpdatePeriod = helpers.DefaultDuration(d.UpdatePeriod, defaultUpdatePeriod)
|
||||
d.Unbound.setDefaults()
|
||||
d.Blacklist.setDefaults()
|
||||
}
|
||||
|
||||
func (d DoT) String() string {
|
||||
return d.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (d DoT) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("DNS over TLS settings:")
|
||||
|
||||
node.Appendf("Enabled: %s", helpers.BoolPtrToYesNo(d.Enabled))
|
||||
if !*d.Enabled {
|
||||
return node
|
||||
}
|
||||
|
||||
update := "disabled"
|
||||
if *d.UpdatePeriod > 0 {
|
||||
update = "every " + d.UpdatePeriod.String()
|
||||
}
|
||||
node.Appendf("Update period: %s", update)
|
||||
|
||||
node.AppendNode(d.Unbound.toLinesNode())
|
||||
node.AppendNode(d.Blacklist.toLinesNode())
|
||||
|
||||
return node
|
||||
}
|
||||
51
internal/configuration/settings/errors.go
Normal file
51
internal/configuration/settings/errors.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package settings
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrCityNotValid = errors.New("the city specified is not valid")
|
||||
ErrControlServerPrivilegedPort = errors.New("cannot use privileged port without running as root")
|
||||
ErrCountryNotValid = errors.New("the country specified is not valid")
|
||||
ErrFirewallZeroPort = errors.New("cannot have a zero port to block")
|
||||
ErrHostnameNotValid = errors.New("the hostname specified is not valid")
|
||||
ErrISPNotValid = errors.New("the ISP specified is not valid")
|
||||
ErrNameNotValid = errors.New("the server name specified is not valid")
|
||||
ErrOpenVPNClientCertMissing = errors.New("client certificate is missing")
|
||||
ErrOpenVPNClientCertNotValid = errors.New("client certificate is not valid")
|
||||
ErrOpenVPNClientKeyMissing = errors.New("client key is missing")
|
||||
ErrOpenVPNClientKeyNotValid = errors.New("client key is not valid")
|
||||
ErrOpenVPNConfigFile = errors.New("custom configuration file error")
|
||||
ErrOpenVPNCustomPortNotAllowed = errors.New("custom endpoint port is not allowed")
|
||||
ErrOpenVPNEncryptionPresetNotValid = errors.New("PIA encryption preset is not valid")
|
||||
ErrOpenVPNInterfaceNotValid = errors.New("interface name is not valid")
|
||||
ErrOpenVPNMSSFixIsTooHigh = errors.New("mssfix option value is too high")
|
||||
ErrOpenVPNPasswordIsEmpty = errors.New("password is empty")
|
||||
ErrOpenVPNTCPNotSupported = errors.New("TCP protocol is not supported")
|
||||
ErrOpenVPNUserIsEmpty = errors.New("user is empty")
|
||||
ErrOpenVPNVerbosityIsOutOfBounds = errors.New("verbosity value is out of bounds")
|
||||
ErrOpenVPNVersionIsNotValid = errors.New("version is not valid")
|
||||
ErrPortForwardingEnabled = errors.New("port forwarding cannot be enabled")
|
||||
ErrPortForwardingFilepathNotValid = errors.New("port forwarding filepath given is not valid")
|
||||
ErrPublicIPFilepathNotValid = errors.New("public IP address file path is not valid")
|
||||
ErrPublicIPPeriodTooShort = errors.New("public IP address check period is too short")
|
||||
ErrRegionNotValid = errors.New("the region specified is not valid")
|
||||
ErrServerAddressNotValid = errors.New("server listening address is not valid")
|
||||
ErrSystemPGIDNotValid = errors.New("process group id is not valid")
|
||||
ErrSystemPUIDNotValid = errors.New("process user id is not valid")
|
||||
ErrSystemTimezoneNotValid = errors.New("timezone is not valid")
|
||||
ErrVPNProviderNameNotValid = errors.New("VPN provider name is not valid")
|
||||
ErrVPNTypeNotValid = errors.New("VPN type is not valid")
|
||||
ErrWireguardEndpointIPNotSet = errors.New("endpoint IP is not set")
|
||||
ErrWireguardEndpointPortNotAllowed = errors.New("endpoint port is not allowed")
|
||||
ErrWireguardEndpointPortNotSet = errors.New("endpoint port is not set")
|
||||
ErrWireguardInterfaceAddressNotSet = errors.New("interface address is not set")
|
||||
ErrWireguardInterfaceNotValid = errors.New("interface name is not valid")
|
||||
ErrWireguardPreSharedKeyNotSet = errors.New("pre-shared key is not set")
|
||||
ErrWireguardPreSharedKeyNotValid = errors.New("pre-shared key is not valid")
|
||||
ErrWireguardPrivateKeyNotSet = errors.New("private key is not set")
|
||||
ErrWireguardPrivateKeyNotValid = errors.New("private key is not valid")
|
||||
ErrWireguardPublicKeyNotSet = errors.New("public key is not set")
|
||||
ErrWireguardPublicKeyNotValid = errors.New("public key is not valid")
|
||||
|
||||
ErrUpdaterPeriodTooSmall = errors.New("VPN server data updater period is too small")
|
||||
)
|
||||
117
internal/configuration/settings/firewall.go
Normal file
117
internal/configuration/settings/firewall.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
// Firewall contains settings to customize the firewall operation.
|
||||
type Firewall struct {
|
||||
VPNInputPorts []uint16
|
||||
InputPorts []uint16
|
||||
OutboundSubnets []net.IPNet
|
||||
Enabled *bool
|
||||
Debug *bool
|
||||
}
|
||||
|
||||
func (f Firewall) validate() (err error) {
|
||||
if hasZeroPort(f.VPNInputPorts) {
|
||||
return fmt.Errorf("VPN input ports: %w", ErrFirewallZeroPort)
|
||||
}
|
||||
|
||||
if hasZeroPort(f.InputPorts) {
|
||||
return fmt.Errorf("input ports: %w", ErrFirewallZeroPort)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasZeroPort(ports []uint16) (has bool) {
|
||||
for _, port := range ports {
|
||||
if port == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (f *Firewall) copy() (copied Firewall) {
|
||||
return Firewall{
|
||||
VPNInputPorts: helpers.CopyUint16Slice(f.VPNInputPorts),
|
||||
InputPorts: helpers.CopyUint16Slice(f.InputPorts),
|
||||
OutboundSubnets: helpers.CopyIPNetSlice(f.OutboundSubnets),
|
||||
Enabled: helpers.CopyBoolPtr(f.Enabled),
|
||||
Debug: helpers.CopyBoolPtr(f.Debug),
|
||||
}
|
||||
}
|
||||
|
||||
// mergeWith merges the other settings into any
|
||||
// unset field of the receiver settings object.
|
||||
// It merges values of slices together, even if they
|
||||
// are set in the receiver settings.
|
||||
func (f *Firewall) mergeWith(other Firewall) {
|
||||
f.VPNInputPorts = helpers.MergeUint16Slices(f.VPNInputPorts, other.VPNInputPorts)
|
||||
f.InputPorts = helpers.MergeUint16Slices(f.InputPorts, other.InputPorts)
|
||||
f.OutboundSubnets = helpers.MergeIPNetsSlices(f.OutboundSubnets, other.OutboundSubnets)
|
||||
f.Enabled = helpers.MergeWithBool(f.Enabled, other.Enabled)
|
||||
f.Debug = helpers.MergeWithBool(f.Debug, other.Debug)
|
||||
}
|
||||
|
||||
// overrideWith overrides fields of the receiver
|
||||
// settings object with any field set in the other
|
||||
// settings.
|
||||
func (f *Firewall) overrideWith(other Firewall) {
|
||||
f.VPNInputPorts = helpers.OverrideWithUint16Slice(f.VPNInputPorts, other.VPNInputPorts)
|
||||
f.InputPorts = helpers.OverrideWithUint16Slice(f.InputPorts, other.InputPorts)
|
||||
f.OutboundSubnets = helpers.OverrideWithIPNetsSlice(f.OutboundSubnets, other.OutboundSubnets)
|
||||
f.Enabled = helpers.OverrideWithBool(f.Enabled, other.Enabled)
|
||||
f.Debug = helpers.OverrideWithBool(f.Debug, other.Debug)
|
||||
}
|
||||
|
||||
func (f *Firewall) setDefaults() {
|
||||
f.Enabled = helpers.DefaultBool(f.Enabled, true)
|
||||
f.Debug = helpers.DefaultBool(f.Debug, false)
|
||||
}
|
||||
|
||||
func (f Firewall) String() string {
|
||||
return f.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (f Firewall) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("Firewall settings:")
|
||||
|
||||
node.Appendf("Enabled: %s", helpers.BoolPtrToYesNo(f.Enabled))
|
||||
if !*f.Enabled {
|
||||
return node
|
||||
}
|
||||
|
||||
if *f.Debug {
|
||||
node.Appendf("Debug mode: on")
|
||||
}
|
||||
|
||||
if len(f.VPNInputPorts) > 0 {
|
||||
vpnInputPortsNode := node.Appendf("VPN input ports:")
|
||||
for _, port := range f.VPNInputPorts {
|
||||
vpnInputPortsNode.Appendf("%d", port)
|
||||
}
|
||||
}
|
||||
|
||||
if len(f.InputPorts) > 0 {
|
||||
inputPortsNode := node.Appendf("Input ports:")
|
||||
for _, port := range f.InputPorts {
|
||||
inputPortsNode.Appendf("%d", port)
|
||||
}
|
||||
}
|
||||
|
||||
if len(f.OutboundSubnets) > 0 {
|
||||
outboundSubnets := node.Appendf("Outbound subnets:")
|
||||
for _, subnet := range f.OutboundSubnets {
|
||||
outboundSubnets.Appendf("%s", subnet)
|
||||
}
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
83
internal/configuration/settings/health.go
Normal file
83
internal/configuration/settings/health.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gotree"
|
||||
"github.com/qdm12/govalid/address"
|
||||
)
|
||||
|
||||
// Health contains settings for the healthcheck and health server.
|
||||
type Health struct {
|
||||
// ServerAddress is the listening address
|
||||
// for the health check server.
|
||||
// It cannot be the empty string in the internal state.
|
||||
ServerAddress string
|
||||
// AddressToPing is the IP address or domain name to
|
||||
// ping periodically for the health check.
|
||||
// It cannot be the empty string in the internal state.
|
||||
AddressToPing string
|
||||
VPN HealthyWait
|
||||
}
|
||||
|
||||
func (h Health) Validate() (err error) {
|
||||
uid := os.Getuid()
|
||||
_, err = address.Validate(h.ServerAddress,
|
||||
address.OptionListening(uid))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %s",
|
||||
ErrServerAddressNotValid, err)
|
||||
}
|
||||
|
||||
err = h.VPN.validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("health VPN settings validation failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Health) copy() (copied Health) {
|
||||
return Health{
|
||||
ServerAddress: h.ServerAddress,
|
||||
AddressToPing: h.AddressToPing,
|
||||
VPN: h.VPN.copy(),
|
||||
}
|
||||
}
|
||||
|
||||
// MergeWith merges the other settings into any
|
||||
// unset field of the receiver settings object.
|
||||
func (h *Health) MergeWith(other Health) {
|
||||
h.ServerAddress = helpers.MergeWithString(h.ServerAddress, other.ServerAddress)
|
||||
h.AddressToPing = helpers.MergeWithString(h.AddressToPing, other.AddressToPing)
|
||||
h.VPN.mergeWith(other.VPN)
|
||||
}
|
||||
|
||||
// OverrideWith overrides fields of the receiver
|
||||
// settings object with any field set in the other
|
||||
// settings.
|
||||
func (h *Health) OverrideWith(other Health) {
|
||||
h.ServerAddress = helpers.OverrideWithString(h.ServerAddress, other.ServerAddress)
|
||||
h.AddressToPing = helpers.OverrideWithString(h.AddressToPing, other.AddressToPing)
|
||||
h.VPN.overrideWith(other.VPN)
|
||||
}
|
||||
|
||||
func (h *Health) SetDefaults() {
|
||||
h.ServerAddress = helpers.DefaultString(h.ServerAddress, "127.0.0.1:9999")
|
||||
h.AddressToPing = helpers.DefaultString(h.AddressToPing, "github.com")
|
||||
h.VPN.setDefaults()
|
||||
}
|
||||
|
||||
func (h Health) String() string {
|
||||
return h.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (h Health) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("Health settings:")
|
||||
node.Appendf("Server listening address: %s", h.ServerAddress)
|
||||
node.Appendf("Address to ping: %s", h.AddressToPing)
|
||||
node.AppendNode(h.VPN.toLinesNode("VPN"))
|
||||
return node
|
||||
}
|
||||
66
internal/configuration/settings/healthywait.go
Normal file
66
internal/configuration/settings/healthywait.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
type HealthyWait struct {
|
||||
// Initial is the initial duration to wait for the program
|
||||
// to be healthy before taking action.
|
||||
// It cannot be nil in the internal state.
|
||||
Initial *time.Duration
|
||||
// Addition is the duration to add to the Initial duration
|
||||
// after Initial has expired to wait longer for the program
|
||||
// to be healthy.
|
||||
// It cannot be nil in the internal state.
|
||||
Addition *time.Duration
|
||||
}
|
||||
|
||||
func (h HealthyWait) validate() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// mergeWith merges the other settings into any
|
||||
// unset field of the receiver settings object.
|
||||
func (h *HealthyWait) copy() (copied HealthyWait) {
|
||||
return HealthyWait{
|
||||
Initial: helpers.CopyDurationPtr(h.Initial),
|
||||
Addition: helpers.CopyDurationPtr(h.Addition),
|
||||
}
|
||||
}
|
||||
|
||||
// mergeWith merges the other settings into any
|
||||
// unset field of the receiver settings object.
|
||||
func (h *HealthyWait) mergeWith(other HealthyWait) {
|
||||
h.Initial = helpers.MergeWithDuration(h.Initial, other.Initial)
|
||||
h.Addition = helpers.MergeWithDuration(h.Addition, other.Addition)
|
||||
}
|
||||
|
||||
// overrideWith overrides fields of the receiver
|
||||
// settings object with any field set in the other
|
||||
// settings.
|
||||
func (h *HealthyWait) overrideWith(other HealthyWait) {
|
||||
h.Initial = helpers.OverrideWithDuration(h.Initial, other.Initial)
|
||||
h.Addition = helpers.OverrideWithDuration(h.Addition, other.Addition)
|
||||
}
|
||||
|
||||
func (h *HealthyWait) setDefaults() {
|
||||
const initialDurationDefault = 6 * time.Second
|
||||
const additionDurationDefault = 5 * time.Second
|
||||
h.Initial = helpers.DefaultDuration(h.Initial, initialDurationDefault)
|
||||
h.Addition = helpers.DefaultDuration(h.Addition, additionDurationDefault)
|
||||
}
|
||||
|
||||
func (h HealthyWait) String() string {
|
||||
return h.toLinesNode("Health").String()
|
||||
}
|
||||
|
||||
func (h HealthyWait) toLinesNode(kind string) (node *gotree.Node) {
|
||||
node = gotree.New(kind + " wait durations:")
|
||||
node.Appendf("Initial duration: %s", *h.Initial)
|
||||
node.Appendf("Additional duration: %s", *h.Addition)
|
||||
return node
|
||||
}
|
||||
45
internal/configuration/settings/helpers/belong.go
Normal file
45
internal/configuration/settings/helpers/belong.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func IsOneOf(value string, choices ...string) (ok bool) {
|
||||
for _, choice := range choices {
|
||||
if value == choice {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var ErrValueNotOneOf = errors.New("value is not one of the possible choices")
|
||||
|
||||
func AreAllOneOf(values, choices []string) (err error) {
|
||||
set := make(map[string]struct{}, len(choices))
|
||||
for _, choice := range choices {
|
||||
choice = strings.ToLower(choice)
|
||||
set[choice] = struct{}{}
|
||||
}
|
||||
|
||||
for _, value := range values {
|
||||
_, ok := set[value]
|
||||
if !ok {
|
||||
return fmt.Errorf("%w: value %q, choices available are %s",
|
||||
ErrValueNotOneOf, value, strings.Join(choices, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Uint16IsOneOf(port uint16, choices []uint16) (ok bool) {
|
||||
for _, choice := range choices {
|
||||
if port == choice {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
190
internal/configuration/settings/helpers/copy.go
Normal file
190
internal/configuration/settings/helpers/copy.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/golibs/logging"
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
func CopyStringPtr(original *string) (copied *string) {
|
||||
if original == nil {
|
||||
return nil
|
||||
}
|
||||
copied = new(string)
|
||||
*copied = *original
|
||||
return copied
|
||||
}
|
||||
|
||||
func CopyBoolPtr(original *bool) (copied *bool) {
|
||||
if original == nil {
|
||||
return nil
|
||||
}
|
||||
copied = new(bool)
|
||||
*copied = *original
|
||||
return copied
|
||||
}
|
||||
|
||||
func CopyUint8Ptr(original *uint8) (copied *uint8) {
|
||||
if original == nil {
|
||||
return nil
|
||||
}
|
||||
copied = new(uint8)
|
||||
*copied = *original
|
||||
return copied
|
||||
}
|
||||
|
||||
func CopyUint16Ptr(original *uint16) (copied *uint16) {
|
||||
if original == nil {
|
||||
return nil
|
||||
}
|
||||
copied = new(uint16)
|
||||
*copied = *original
|
||||
return copied
|
||||
}
|
||||
|
||||
func CopyIntPtr(original *int) (copied *int) {
|
||||
if original == nil {
|
||||
return nil
|
||||
}
|
||||
copied = new(int)
|
||||
*copied = *original
|
||||
return copied
|
||||
}
|
||||
|
||||
func CopyDurationPtr(original *time.Duration) (copied *time.Duration) {
|
||||
if original == nil {
|
||||
return nil
|
||||
}
|
||||
copied = new(time.Duration)
|
||||
*copied = *original
|
||||
return copied
|
||||
}
|
||||
|
||||
func CopyLogLevelPtr(original *logging.Level) (copied *logging.Level) {
|
||||
if original == nil {
|
||||
return nil
|
||||
}
|
||||
copied = new(logging.Level)
|
||||
*copied = *original
|
||||
return copied
|
||||
}
|
||||
|
||||
func CopyIP(original net.IP) (copied net.IP) {
|
||||
if original == nil {
|
||||
return nil
|
||||
}
|
||||
copied = make(net.IP, len(original))
|
||||
copy(copied, original)
|
||||
return copied
|
||||
}
|
||||
|
||||
func CopyIPNet(original net.IPNet) (copied net.IPNet) {
|
||||
if original.IP != nil {
|
||||
copied.IP = make(net.IP, len(original.IP))
|
||||
copy(copied.IP, original.IP)
|
||||
}
|
||||
|
||||
if original.Mask != nil {
|
||||
copied.Mask = make(net.IPMask, len(original.Mask))
|
||||
copy(copied.Mask, original.Mask)
|
||||
}
|
||||
|
||||
return copied
|
||||
}
|
||||
|
||||
func CopyIPNetPtr(original *net.IPNet) (copied *net.IPNet) {
|
||||
if original == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
copied = new(net.IPNet)
|
||||
*copied = CopyIPNet(*original)
|
||||
return copied
|
||||
}
|
||||
|
||||
func CopyNetaddrIP(original netaddr.IP) (copied netaddr.IP) {
|
||||
b, err := original.MarshalBinary()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = copied.UnmarshalBinary(b)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return copied
|
||||
}
|
||||
|
||||
func CopyIPPrefix(original netaddr.IPPrefix) (copied netaddr.IPPrefix) {
|
||||
b, err := original.MarshalText()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = copied.UnmarshalText(b)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return copied
|
||||
}
|
||||
|
||||
func CopyStringSlice(original []string) (copied []string) {
|
||||
if original == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
copied = make([]string, len(original))
|
||||
copy(copied, original)
|
||||
return copied
|
||||
}
|
||||
|
||||
func CopyUint16Slice(original []uint16) (copied []uint16) {
|
||||
if original == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
copied = make([]uint16, len(original))
|
||||
copy(copied, original)
|
||||
return copied
|
||||
}
|
||||
|
||||
func CopyIPNetSlice(original []net.IPNet) (copied []net.IPNet) {
|
||||
if original == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
copied = make([]net.IPNet, len(original))
|
||||
for i := range original {
|
||||
copied[i] = CopyIPNet(original[i])
|
||||
}
|
||||
return copied
|
||||
}
|
||||
|
||||
func CopyIPPrefixSlice(original []netaddr.IPPrefix) (copied []netaddr.IPPrefix) {
|
||||
if original == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
copied = make([]netaddr.IPPrefix, len(original))
|
||||
for i := range original {
|
||||
copied[i] = CopyIPPrefix(original[i])
|
||||
}
|
||||
return copied
|
||||
}
|
||||
|
||||
func CopyNetaddrIPsSlice(original []netaddr.IP) (copied []netaddr.IP) {
|
||||
if original == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
copied = make([]netaddr.IP, len(original))
|
||||
for i := range original {
|
||||
copied[i] = CopyNetaddrIP(original[i])
|
||||
}
|
||||
|
||||
return copied
|
||||
}
|
||||
93
internal/configuration/settings/helpers/default.go
Normal file
93
internal/configuration/settings/helpers/default.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/golibs/logging"
|
||||
)
|
||||
|
||||
func DefaultInt(existing *int, defaultValue int) (
|
||||
result *int) {
|
||||
if existing != nil {
|
||||
return existing
|
||||
}
|
||||
result = new(int)
|
||||
*result = defaultValue
|
||||
return result
|
||||
}
|
||||
|
||||
func DefaultUint8(existing *uint8, defaultValue uint8) (
|
||||
result *uint8) {
|
||||
if existing != nil {
|
||||
return existing
|
||||
}
|
||||
result = new(uint8)
|
||||
*result = defaultValue
|
||||
return result
|
||||
}
|
||||
|
||||
func DefaultUint16(existing *uint16, defaultValue uint16) (
|
||||
result *uint16) {
|
||||
if existing != nil {
|
||||
return existing
|
||||
}
|
||||
result = new(uint16)
|
||||
*result = defaultValue
|
||||
return result
|
||||
}
|
||||
|
||||
func DefaultBool(existing *bool, defaultValue bool) (
|
||||
result *bool) {
|
||||
if existing != nil {
|
||||
return existing
|
||||
}
|
||||
result = new(bool)
|
||||
*result = defaultValue
|
||||
return result
|
||||
}
|
||||
|
||||
func DefaultString(existing string, defaultValue string) (
|
||||
result string) {
|
||||
if existing != "" {
|
||||
return existing
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func DefaultStringPtr(existing *string, defaultValue string) (result *string) {
|
||||
if existing != nil {
|
||||
return existing
|
||||
}
|
||||
result = new(string)
|
||||
*result = defaultValue
|
||||
return result
|
||||
}
|
||||
|
||||
func DefaultDuration(existing *time.Duration,
|
||||
defaultValue time.Duration) (result *time.Duration) {
|
||||
if existing != nil {
|
||||
return existing
|
||||
}
|
||||
result = new(time.Duration)
|
||||
*result = defaultValue
|
||||
return result
|
||||
}
|
||||
|
||||
func DefaultLogLevel(existing *logging.Level,
|
||||
defaultValue logging.Level) (result *logging.Level) {
|
||||
if existing != nil {
|
||||
return existing
|
||||
}
|
||||
result = new(logging.Level)
|
||||
*result = defaultValue
|
||||
return result
|
||||
}
|
||||
|
||||
func DefaultIP(existing net.IP, defaultValue net.IP) (
|
||||
result net.IP) {
|
||||
if existing != nil {
|
||||
return existing
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
31
internal/configuration/settings/helpers/files.go
Normal file
31
internal/configuration/settings/helpers/files.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrFileDoesNotExist = errors.New("file does not exist")
|
||||
ErrFileRead = errors.New("cannot read file")
|
||||
ErrFileClose = errors.New("cannot close file")
|
||||
)
|
||||
|
||||
func FileExists(path string) (err error) {
|
||||
path = filepath.Clean(path)
|
||||
|
||||
f, err := os.Open(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("%w: %s", ErrFileDoesNotExist, path)
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("%w: %s", ErrFileRead, err)
|
||||
}
|
||||
|
||||
if err := f.Close(); err != nil {
|
||||
return fmt.Errorf("%w: %s", ErrFileClose, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
226
internal/configuration/settings/helpers/merge.go
Normal file
226
internal/configuration/settings/helpers/merge.go
Normal file
@@ -0,0 +1,226 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/golibs/logging"
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
func MergeWithBool(existing, other *bool) (result *bool) {
|
||||
if existing != nil {
|
||||
return existing
|
||||
} else if other == nil {
|
||||
return nil
|
||||
}
|
||||
result = new(bool)
|
||||
*result = *other
|
||||
return result
|
||||
}
|
||||
|
||||
func MergeWithString(existing, other string) (result string) {
|
||||
if existing != "" {
|
||||
return existing
|
||||
}
|
||||
return other
|
||||
}
|
||||
|
||||
func MergeWithStringPtr(existing, other *string) (result *string) {
|
||||
if existing != nil {
|
||||
return existing
|
||||
} else if other == nil {
|
||||
return nil
|
||||
}
|
||||
result = new(string)
|
||||
*result = *other
|
||||
return result
|
||||
}
|
||||
|
||||
func MergeWithInt(existing, other *int) (result *int) {
|
||||
if existing != nil {
|
||||
return existing
|
||||
} else if other == nil {
|
||||
return nil
|
||||
}
|
||||
result = new(int)
|
||||
*result = *other
|
||||
return result
|
||||
}
|
||||
|
||||
func MergeWithUint8(existing, other *uint8) (result *uint8) {
|
||||
if existing != nil {
|
||||
return existing
|
||||
} else if other == nil {
|
||||
return nil
|
||||
}
|
||||
result = new(uint8)
|
||||
*result = *other
|
||||
return result
|
||||
}
|
||||
|
||||
func MergeWithUint16(existing, other *uint16) (result *uint16) {
|
||||
if existing != nil {
|
||||
return existing
|
||||
} else if other == nil {
|
||||
return nil
|
||||
}
|
||||
result = new(uint16)
|
||||
*result = *other
|
||||
return result
|
||||
}
|
||||
|
||||
func MergeWithIP(existing, other net.IP) (result net.IP) {
|
||||
if existing != nil {
|
||||
return existing
|
||||
} else if other == nil {
|
||||
return nil
|
||||
}
|
||||
result = make(net.IP, len(other))
|
||||
copy(result, other)
|
||||
return result
|
||||
}
|
||||
|
||||
func MergeWithDuration(existing, other *time.Duration) (result *time.Duration) {
|
||||
if existing != nil {
|
||||
return existing
|
||||
}
|
||||
return other
|
||||
}
|
||||
|
||||
func MergeWithLogLevel(existing, other *logging.Level) (result *logging.Level) {
|
||||
if existing != nil {
|
||||
return existing
|
||||
} else if other == nil {
|
||||
return nil
|
||||
}
|
||||
result = new(logging.Level)
|
||||
*result = *other
|
||||
return result
|
||||
}
|
||||
|
||||
func MergeStringSlices(a, b []string) (result []string) {
|
||||
if a == nil && b == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{}, len(a)+len(b))
|
||||
result = make([]string, 0, len(a)+len(b))
|
||||
for _, s := range a {
|
||||
if _, ok := seen[s]; ok {
|
||||
continue // duplicate
|
||||
}
|
||||
result = append(result, s)
|
||||
seen[s] = struct{}{}
|
||||
}
|
||||
for _, s := range b {
|
||||
if _, ok := seen[s]; ok {
|
||||
continue // duplicate
|
||||
}
|
||||
result = append(result, s)
|
||||
seen[s] = struct{}{}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func MergeUint16Slices(a, b []uint16) (result []uint16) {
|
||||
if a == nil && b == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
seen := make(map[uint16]struct{}, len(a)+len(b))
|
||||
result = make([]uint16, 0, len(a)+len(b))
|
||||
for _, n := range a {
|
||||
if _, ok := seen[n]; ok {
|
||||
continue // duplicate
|
||||
}
|
||||
result = append(result, n)
|
||||
seen[n] = struct{}{}
|
||||
}
|
||||
for _, n := range b {
|
||||
if _, ok := seen[n]; ok {
|
||||
continue // duplicate
|
||||
}
|
||||
result = append(result, n)
|
||||
seen[n] = struct{}{}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func MergeIPNetsSlices(a, b []net.IPNet) (result []net.IPNet) {
|
||||
if a == nil && b == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{}, len(a)+len(b))
|
||||
result = make([]net.IPNet, 0, len(a)+len(b))
|
||||
for _, ipNet := range a {
|
||||
key := ipNet.String()
|
||||
if _, ok := seen[key]; ok {
|
||||
continue // duplicate
|
||||
}
|
||||
result = append(result, ipNet)
|
||||
seen[key] = struct{}{}
|
||||
}
|
||||
for _, ipNet := range b {
|
||||
key := ipNet.String()
|
||||
if _, ok := seen[key]; ok {
|
||||
continue // duplicate
|
||||
}
|
||||
result = append(result, ipNet)
|
||||
seen[key] = struct{}{}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func MergeNetaddrIPsSlices(a, b []netaddr.IP) (result []netaddr.IP) {
|
||||
if a == nil && b == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{}, len(a)+len(b))
|
||||
result = make([]netaddr.IP, 0, len(a)+len(b))
|
||||
for _, ip := range a {
|
||||
key := ip.String()
|
||||
if _, ok := seen[key]; ok {
|
||||
continue // duplicate
|
||||
}
|
||||
result = append(result, ip)
|
||||
seen[key] = struct{}{}
|
||||
}
|
||||
for _, ip := range b {
|
||||
key := ip.String()
|
||||
if _, ok := seen[key]; ok {
|
||||
continue // duplicate
|
||||
}
|
||||
result = append(result, ip)
|
||||
seen[key] = struct{}{}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func MergeIPPrefixesSlices(a, b []netaddr.IPPrefix) (result []netaddr.IPPrefix) {
|
||||
if a == nil && b == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{}, len(a)+len(b))
|
||||
result = make([]netaddr.IPPrefix, 0, len(a)+len(b))
|
||||
for _, ipPrefix := range a {
|
||||
key := ipPrefix.String()
|
||||
if _, ok := seen[key]; ok {
|
||||
continue // duplicate
|
||||
}
|
||||
result = append(result, ipPrefix)
|
||||
seen[key] = struct{}{}
|
||||
}
|
||||
for _, ipPrefix := range b {
|
||||
key := ipPrefix.String()
|
||||
if _, ok := seen[key]; ok {
|
||||
continue // duplicate
|
||||
}
|
||||
result = append(result, ipPrefix)
|
||||
seen[key] = struct{}{}
|
||||
}
|
||||
return result
|
||||
}
|
||||
29
internal/configuration/settings/helpers/messages.go
Normal file
29
internal/configuration/settings/helpers/messages.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ChoicesOrString(choices []string) string {
|
||||
return strings.Join(
|
||||
choices[:len(choices)-1], ", ") +
|
||||
" or " + choices[len(choices)-1]
|
||||
}
|
||||
|
||||
func PortChoicesOrString(ports []uint16) (s string) {
|
||||
switch len(ports) {
|
||||
case 0:
|
||||
return "there is no allowed port"
|
||||
case 1:
|
||||
return "allowed port is " + fmt.Sprint(ports[0])
|
||||
}
|
||||
|
||||
s = "allowed ports are "
|
||||
portStrings := make([]string, len(ports))
|
||||
for i := range ports {
|
||||
portStrings[i] = fmt.Sprint(ports[i])
|
||||
}
|
||||
s += ChoicesOrString(portStrings)
|
||||
return s
|
||||
}
|
||||
25
internal/configuration/settings/helpers/obfuscate.go
Normal file
25
internal/configuration/settings/helpers/obfuscate.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package helpers
|
||||
|
||||
func ObfuscateWireguardKey(fullKey string) (obfuscatedKey string) {
|
||||
const minKeyLength = 10
|
||||
if len(fullKey) < minKeyLength {
|
||||
return "(too short)"
|
||||
}
|
||||
|
||||
lastIndex := len(fullKey) - 1
|
||||
return fullKey[0:2] + "..." + fullKey[lastIndex-2:]
|
||||
}
|
||||
|
||||
func ObfuscatePassword(password string) (obfuscatedPassword string) {
|
||||
if password != "" {
|
||||
return "[set]"
|
||||
}
|
||||
return "[not set]"
|
||||
}
|
||||
|
||||
func ObfuscateData(data string) (obfuscated string) {
|
||||
if data != "" {
|
||||
return "[set]"
|
||||
}
|
||||
return "[not set]"
|
||||
}
|
||||
133
internal/configuration/settings/helpers/override.go
Normal file
133
internal/configuration/settings/helpers/override.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/golibs/logging"
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
func OverrideWithBool(existing, other *bool) (result *bool) {
|
||||
if other == nil {
|
||||
return existing
|
||||
}
|
||||
result = new(bool)
|
||||
*result = *other
|
||||
return result
|
||||
}
|
||||
|
||||
func OverrideWithString(existing, other string) (result string) {
|
||||
if other == "" {
|
||||
return existing
|
||||
}
|
||||
return other
|
||||
}
|
||||
|
||||
func OverrideWithStringPtr(existing, other *string) (result *string) {
|
||||
if other == nil {
|
||||
return existing
|
||||
}
|
||||
result = new(string)
|
||||
*result = *other
|
||||
return result
|
||||
}
|
||||
|
||||
func OverrideWithInt(existing, other *int) (result *int) {
|
||||
if other == nil {
|
||||
return existing
|
||||
}
|
||||
result = new(int)
|
||||
*result = *other
|
||||
return result
|
||||
}
|
||||
|
||||
func OverrideWithUint8(existing, other *uint8) (result *uint8) {
|
||||
if other == nil {
|
||||
return existing
|
||||
}
|
||||
result = new(uint8)
|
||||
*result = *other
|
||||
return result
|
||||
}
|
||||
|
||||
func OverrideWithUint16(existing, other *uint16) (result *uint16) {
|
||||
if other == nil {
|
||||
return existing
|
||||
}
|
||||
result = new(uint16)
|
||||
*result = *other
|
||||
return result
|
||||
}
|
||||
|
||||
func OverrideWithIP(existing, other net.IP) (result net.IP) {
|
||||
if other == nil {
|
||||
return existing
|
||||
}
|
||||
result = make(net.IP, len(other))
|
||||
copy(result, other)
|
||||
return result
|
||||
}
|
||||
|
||||
func OverrideWithDuration(existing, other *time.Duration) (result *time.Duration) {
|
||||
if other == nil {
|
||||
return existing
|
||||
}
|
||||
result = new(time.Duration)
|
||||
*result = *other
|
||||
return result
|
||||
}
|
||||
|
||||
func OverrideWithLogLevel(existing, other *logging.Level) (result *logging.Level) {
|
||||
if other == nil {
|
||||
return existing
|
||||
}
|
||||
result = new(logging.Level)
|
||||
*result = *other
|
||||
return result
|
||||
}
|
||||
|
||||
func OverrideWithStringSlice(existing, other []string) (result []string) {
|
||||
if other == nil {
|
||||
return existing
|
||||
}
|
||||
result = make([]string, len(other))
|
||||
copy(result, other)
|
||||
return result
|
||||
}
|
||||
|
||||
func OverrideWithUint16Slice(existing, other []uint16) (result []uint16) {
|
||||
if other == nil {
|
||||
return existing
|
||||
}
|
||||
result = make([]uint16, len(other))
|
||||
copy(result, other)
|
||||
return result
|
||||
}
|
||||
|
||||
func OverrideWithIPNetsSlice(existing, other []net.IPNet) (result []net.IPNet) {
|
||||
if other == nil {
|
||||
return existing
|
||||
}
|
||||
result = make([]net.IPNet, len(other))
|
||||
copy(result, other)
|
||||
return result
|
||||
}
|
||||
|
||||
func OverrideWithNetaddrIPsSlice(existing, other []netaddr.IP) (result []netaddr.IP) {
|
||||
if other == nil {
|
||||
return existing
|
||||
}
|
||||
result = make([]netaddr.IP, len(other))
|
||||
copy(result, other)
|
||||
return result
|
||||
}
|
||||
|
||||
func OverrideWithIPPrefixesSlice(existing, other []netaddr.IPPrefix) (result []netaddr.IPPrefix) {
|
||||
if other == nil {
|
||||
return existing
|
||||
}
|
||||
result = make([]netaddr.IPPrefix, len(other))
|
||||
copy(result, other)
|
||||
return result
|
||||
}
|
||||
11
internal/configuration/settings/helpers/pointers.go
Normal file
11
internal/configuration/settings/helpers/pointers.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package helpers
|
||||
|
||||
import "time"
|
||||
|
||||
// StringPtr returns a pointer to the string value
|
||||
// passed as argument.
|
||||
func StringPtr(s string) *string { return &s }
|
||||
|
||||
// DurationPtr returns a pointer to the duration value
|
||||
// passed as argument.
|
||||
func DurationPtr(d time.Duration) *time.Duration { return &d }
|
||||
15
internal/configuration/settings/helpers/string.go
Normal file
15
internal/configuration/settings/helpers/string.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package helpers
|
||||
|
||||
func BoolPtrToYesNo(b *bool) string {
|
||||
if *b {
|
||||
return "yes"
|
||||
}
|
||||
return "no"
|
||||
}
|
||||
|
||||
func TCPPtrToString(tcp *bool) string {
|
||||
if *tcp {
|
||||
return "TCP"
|
||||
}
|
||||
return "UDP"
|
||||
}
|
||||
4
internal/configuration/settings/helpers_test.go
Normal file
4
internal/configuration/settings/helpers_test.go
Normal file
@@ -0,0 +1,4 @@
|
||||
package settings
|
||||
|
||||
func boolPtr(b bool) *bool { return &b }
|
||||
func uint8Ptr(n uint8) *uint8 { return &n }
|
||||
112
internal/configuration/settings/httpproxy.go
Normal file
112
internal/configuration/settings/httpproxy.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gotree"
|
||||
"github.com/qdm12/govalid/address"
|
||||
)
|
||||
|
||||
// HTTPProxy contains settings to configure the HTTP proxy.
|
||||
type HTTPProxy struct {
|
||||
// User is the username to use for the HTTP proxy.
|
||||
// It cannot be nil in the internal state.
|
||||
User *string
|
||||
// Password is the password to use for the HTTP proxy.
|
||||
// It cannot be nil in the internal state.
|
||||
Password *string
|
||||
// ListeningAddress is the listening address
|
||||
// of the HTTP proxy server.
|
||||
// It cannot be the empty string in the internal state.
|
||||
ListeningAddress string
|
||||
// Enabled is true if the HTTP proxy server should run,
|
||||
// and false otherwise. It cannot be nil in the
|
||||
// internal state.
|
||||
Enabled *bool
|
||||
// Stealth is true if the HTTP proxy server should hide
|
||||
// each request has been proxied to the destination.
|
||||
// It cannot be nil in the internal state.
|
||||
Stealth *bool
|
||||
// Log is true if the HTTP proxy server should log
|
||||
// each request/response. It cannot be nil in the
|
||||
// internal state.
|
||||
Log *bool
|
||||
}
|
||||
|
||||
func (h HTTPProxy) validate() (err error) {
|
||||
// Do not validate user and password
|
||||
|
||||
uid := os.Getuid()
|
||||
_, err = address.Validate(h.ListeningAddress, address.OptionListening(uid))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %s",
|
||||
ErrServerAddressNotValid, h.ListeningAddress)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *HTTPProxy) copy() (copied HTTPProxy) {
|
||||
return HTTPProxy{
|
||||
User: helpers.CopyStringPtr(h.User),
|
||||
Password: helpers.CopyStringPtr(h.Password),
|
||||
ListeningAddress: h.ListeningAddress,
|
||||
Enabled: helpers.CopyBoolPtr(h.Enabled),
|
||||
Stealth: helpers.CopyBoolPtr(h.Stealth),
|
||||
Log: helpers.CopyBoolPtr(h.Log),
|
||||
}
|
||||
}
|
||||
|
||||
// mergeWith merges the other settings into any
|
||||
// unset field of the receiver settings object.
|
||||
func (h *HTTPProxy) mergeWith(other HTTPProxy) {
|
||||
h.User = helpers.MergeWithStringPtr(h.User, other.User)
|
||||
h.Password = helpers.MergeWithStringPtr(h.Password, other.Password)
|
||||
h.ListeningAddress = helpers.MergeWithString(h.ListeningAddress, other.ListeningAddress)
|
||||
h.Enabled = helpers.MergeWithBool(h.Enabled, other.Enabled)
|
||||
h.Stealth = helpers.MergeWithBool(h.Stealth, other.Stealth)
|
||||
h.Log = helpers.MergeWithBool(h.Log, other.Log)
|
||||
}
|
||||
|
||||
// overrideWith overrides fields of the receiver
|
||||
// settings object with any field set in the other
|
||||
// settings.
|
||||
func (h *HTTPProxy) overrideWith(other HTTPProxy) {
|
||||
h.User = helpers.OverrideWithStringPtr(h.User, other.User)
|
||||
h.Password = helpers.OverrideWithStringPtr(h.Password, other.Password)
|
||||
h.ListeningAddress = helpers.OverrideWithString(h.ListeningAddress, other.ListeningAddress)
|
||||
h.Enabled = helpers.OverrideWithBool(h.Enabled, other.Enabled)
|
||||
h.Stealth = helpers.OverrideWithBool(h.Stealth, other.Stealth)
|
||||
h.Log = helpers.OverrideWithBool(h.Log, other.Log)
|
||||
}
|
||||
|
||||
func (h *HTTPProxy) setDefaults() {
|
||||
h.User = helpers.DefaultStringPtr(h.User, "")
|
||||
h.Password = helpers.DefaultStringPtr(h.Password, "")
|
||||
h.ListeningAddress = helpers.DefaultString(h.ListeningAddress, "")
|
||||
h.Enabled = helpers.DefaultBool(h.Enabled, false)
|
||||
h.Stealth = helpers.DefaultBool(h.Stealth, false)
|
||||
h.Log = helpers.DefaultBool(h.Log, false)
|
||||
}
|
||||
|
||||
func (h HTTPProxy) String() string {
|
||||
return h.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (h HTTPProxy) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("HTTP proxy settings:")
|
||||
node.Appendf("Enabled: %s", helpers.BoolPtrToYesNo(h.Enabled))
|
||||
if !*h.Enabled {
|
||||
return node
|
||||
}
|
||||
|
||||
node.Appendf("Listening address: %s", h.ListeningAddress)
|
||||
node.Appendf("User: %s", *h.User)
|
||||
node.Appendf("Password: %s", helpers.ObfuscatePassword(*h.Password))
|
||||
node.Appendf("Stealth mode: %s", helpers.BoolPtrToYesNo(h.Stealth))
|
||||
node.Appendf("Log: %s", helpers.BoolPtrToYesNo(h.Log))
|
||||
|
||||
return node
|
||||
}
|
||||
51
internal/configuration/settings/log.go
Normal file
51
internal/configuration/settings/log.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
// Log contains settings to configure the logger.
|
||||
type Log struct {
|
||||
// Level is the log level of the logger.
|
||||
// It cannot be nil in the internal state.
|
||||
Level *logging.Level
|
||||
}
|
||||
|
||||
func (l Log) validate() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Log) copy() (copied Log) {
|
||||
return Log{
|
||||
Level: helpers.CopyLogLevelPtr(l.Level),
|
||||
}
|
||||
}
|
||||
|
||||
// mergeWith merges the other settings into any
|
||||
// unset field of the receiver settings object.
|
||||
func (l *Log) mergeWith(other Log) {
|
||||
l.Level = helpers.MergeWithLogLevel(l.Level, other.Level)
|
||||
}
|
||||
|
||||
// overrideWith overrides fields of the receiver
|
||||
// settings object with any field set in the other
|
||||
// settings.
|
||||
func (l *Log) overrideWith(other Log) {
|
||||
l.Level = helpers.OverrideWithLogLevel(l.Level, other.Level)
|
||||
}
|
||||
|
||||
func (l *Log) setDefaults() {
|
||||
l.Level = helpers.DefaultLogLevel(l.Level, logging.LevelInfo)
|
||||
}
|
||||
|
||||
func (l Log) String() string {
|
||||
return l.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (l Log) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("Log settings:")
|
||||
node.Appendf("Log level: %s", l.Level.String())
|
||||
return node
|
||||
}
|
||||
318
internal/configuration/settings/openvpn.go
Normal file
318
internal/configuration/settings/openvpn.go
Normal file
@@ -0,0 +1,318 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gluetun/internal/openvpn/parse"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
// OpenVPN contains settings to configure the OpenVPN client.
|
||||
type OpenVPN struct {
|
||||
// Version is the OpenVPN version to run.
|
||||
// It can only be "2.4" or "2.5".
|
||||
Version string
|
||||
// User is the OpenVPN authentication username.
|
||||
// It cannot be an empty string in the internal state
|
||||
// if OpenVPN is used.
|
||||
User string
|
||||
// Password is the OpenVPN authentication password.
|
||||
// It cannot be an empty string in the internal state
|
||||
// if OpenVPN is used.
|
||||
Password string
|
||||
// ConfFile is a custom OpenVPN configuration file path.
|
||||
// It can be set to the empty string for it to be ignored.
|
||||
// It cannot be nil in the internal state.
|
||||
ConfFile *string
|
||||
// Ciphers is a list of ciphers to use for OpenVPN,
|
||||
// different from the ones specified by the VPN
|
||||
// service provider configuration files.
|
||||
Ciphers []string
|
||||
// Auth is an auth algorithm to use in OpenVPN instead
|
||||
// of the one specified by the VPN service provider.
|
||||
// It cannot be nil in the internal state.
|
||||
// It is ignored if it is set to the empty string.
|
||||
Auth *string
|
||||
// ClientCrt is the OpenVPN client certificate.
|
||||
// This is notably used by Cyberghost.
|
||||
// It can be set to the empty string to be ignored.
|
||||
// It cannot be nil in the internal state.
|
||||
ClientCrt *string
|
||||
// ClientKey is the OpenVPN client key.
|
||||
// This is used by Cyberghost and VPN Unlimited.
|
||||
// It can be set to the empty string to be ignored.
|
||||
// It cannot be nil in the internal state.
|
||||
ClientKey *string
|
||||
// PIAEncPreset is the encryption preset for
|
||||
// Private Internet Access. It can be set to an
|
||||
// empty string for other providers.
|
||||
PIAEncPreset *string
|
||||
// IPv6 is set to true if IPv6 routing should be
|
||||
// set to be tunnel in OpenVPN, and false otherwise.
|
||||
// It cannot be nil in the internal state.
|
||||
IPv6 *bool // TODO automate like with Wireguard
|
||||
// MSSFix is the value (1 to 10000) to set for the
|
||||
// mssfix option for OpenVPN. It is ignored if set to 0.
|
||||
// It cannot be nil in the internal state.
|
||||
MSSFix *uint16
|
||||
// Interface is the OpenVPN device interface name.
|
||||
// It cannot be an empty string in the internal state.
|
||||
Interface string
|
||||
// Root is true if OpenVPN is to be run as root,
|
||||
// and false otherwise. It cannot be nil in the
|
||||
// internal state.
|
||||
Root *bool
|
||||
// ProcUser is the OpenVPN process OS username
|
||||
// to use. It cannot be nil in the internal state.
|
||||
// This is set and injected at runtime.
|
||||
// TODO only use ProcUser and not Root field.
|
||||
ProcUser string
|
||||
// Verbosity is the OpenVPN verbosity level from 0 to 6.
|
||||
// It cannot be nil in the internal state.
|
||||
Verbosity *int
|
||||
// Flags is a slice of additional flags to be passed
|
||||
// to the OpenVPN program.
|
||||
Flags []string
|
||||
}
|
||||
|
||||
func (o OpenVPN) validate(vpnProvider string) (err error) {
|
||||
// Validate version
|
||||
validVersions := []string{constants.Openvpn24, constants.Openvpn25}
|
||||
if !helpers.IsOneOf(o.Version, validVersions...) {
|
||||
return fmt.Errorf("%w: %q can only be one of %s",
|
||||
ErrOpenVPNVersionIsNotValid, o.Version, strings.Join(validVersions, ", "))
|
||||
}
|
||||
|
||||
if o.User == "" {
|
||||
return ErrOpenVPNUserIsEmpty
|
||||
}
|
||||
|
||||
if o.Password == "" {
|
||||
return ErrOpenVPNPasswordIsEmpty
|
||||
}
|
||||
|
||||
// Validate ConfFile
|
||||
if vpnProvider == constants.Custom {
|
||||
if *o.ConfFile == "" {
|
||||
return fmt.Errorf("%w: no file path specified", ErrOpenVPNConfigFile)
|
||||
}
|
||||
err := helpers.FileExists(*o.ConfFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %s", ErrOpenVPNConfigFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check client certificate
|
||||
switch vpnProvider {
|
||||
case
|
||||
constants.Cyberghost,
|
||||
constants.VPNUnlimited:
|
||||
if *o.ClientCrt == "" {
|
||||
return ErrOpenVPNClientCertMissing
|
||||
}
|
||||
}
|
||||
if *o.ClientCrt != "" {
|
||||
_, err = parse.ExtractCert([]byte(*o.ClientCrt))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %s", ErrOpenVPNClientCertNotValid, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check client key
|
||||
switch vpnProvider {
|
||||
case
|
||||
constants.Cyberghost,
|
||||
constants.VPNUnlimited,
|
||||
constants.Wevpn:
|
||||
if *o.ClientKey == "" {
|
||||
return ErrOpenVPNClientKeyMissing
|
||||
}
|
||||
}
|
||||
if *o.ClientKey != "" {
|
||||
_, err = parse.ExtractPrivateKey([]byte(*o.ClientKey))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %s", ErrOpenVPNClientKeyNotValid, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate MSSFix
|
||||
const maxMSSFix = 10000
|
||||
if *o.MSSFix > maxMSSFix {
|
||||
return fmt.Errorf("%w: %d is over the maximum value of %d",
|
||||
ErrOpenVPNMSSFixIsTooHigh, *o.MSSFix, maxMSSFix)
|
||||
}
|
||||
|
||||
if !regexpInterfaceName.MatchString(o.Interface) {
|
||||
return fmt.Errorf("%w: '%s' does not match regex '%s'",
|
||||
ErrOpenVPNInterfaceNotValid, o.Interface, regexpInterfaceName)
|
||||
}
|
||||
|
||||
// Validate Verbosity
|
||||
if *o.Verbosity < 0 || *o.Verbosity > 6 {
|
||||
return fmt.Errorf("%w: %d can only be between 0 and 5",
|
||||
ErrOpenVPNVerbosityIsOutOfBounds, o.Verbosity)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *OpenVPN) copy() (copied OpenVPN) {
|
||||
return OpenVPN{
|
||||
Version: o.Version,
|
||||
User: o.User,
|
||||
Password: o.Password,
|
||||
ConfFile: helpers.CopyStringPtr(o.ConfFile),
|
||||
Ciphers: helpers.CopyStringSlice(o.Ciphers),
|
||||
Auth: helpers.CopyStringPtr(o.Auth),
|
||||
ClientCrt: helpers.CopyStringPtr(o.ClientCrt),
|
||||
ClientKey: helpers.CopyStringPtr(o.ClientKey),
|
||||
PIAEncPreset: helpers.CopyStringPtr(o.PIAEncPreset),
|
||||
IPv6: helpers.CopyBoolPtr(o.IPv6),
|
||||
MSSFix: helpers.CopyUint16Ptr(o.MSSFix),
|
||||
Interface: o.Interface,
|
||||
Root: helpers.CopyBoolPtr(o.Root),
|
||||
ProcUser: o.ProcUser,
|
||||
Verbosity: helpers.CopyIntPtr(o.Verbosity),
|
||||
Flags: helpers.CopyStringSlice(o.Flags),
|
||||
}
|
||||
}
|
||||
|
||||
// mergeWith merges the other settings into any
|
||||
// unset field of the receiver settings object.
|
||||
func (o *OpenVPN) mergeWith(other OpenVPN) {
|
||||
o.Version = helpers.MergeWithString(o.Version, other.Version)
|
||||
o.User = helpers.MergeWithString(o.User, other.User)
|
||||
o.Password = helpers.MergeWithString(o.Password, other.Password)
|
||||
o.ConfFile = helpers.MergeWithStringPtr(o.ConfFile, other.ConfFile)
|
||||
o.Ciphers = helpers.MergeStringSlices(o.Ciphers, other.Ciphers)
|
||||
o.Auth = helpers.MergeWithStringPtr(o.Auth, other.Auth)
|
||||
o.ClientCrt = helpers.MergeWithStringPtr(o.ClientCrt, other.ClientCrt)
|
||||
o.ClientKey = helpers.MergeWithStringPtr(o.ClientKey, other.ClientKey)
|
||||
o.PIAEncPreset = helpers.MergeWithStringPtr(o.PIAEncPreset, other.PIAEncPreset)
|
||||
o.IPv6 = helpers.MergeWithBool(o.IPv6, other.IPv6)
|
||||
o.MSSFix = helpers.MergeWithUint16(o.MSSFix, other.MSSFix)
|
||||
o.Interface = helpers.MergeWithString(o.Interface, other.Interface)
|
||||
o.Root = helpers.MergeWithBool(o.Root, other.Root)
|
||||
o.ProcUser = helpers.MergeWithString(o.ProcUser, other.ProcUser)
|
||||
o.Verbosity = helpers.MergeWithInt(o.Verbosity, other.Verbosity)
|
||||
o.Flags = helpers.MergeStringSlices(o.Flags, other.Flags)
|
||||
}
|
||||
|
||||
// overrideWith overrides fields of the receiver
|
||||
// settings object with any field set in the other
|
||||
// settings.
|
||||
func (o *OpenVPN) overrideWith(other OpenVPN) {
|
||||
o.Version = helpers.OverrideWithString(o.Version, other.Version)
|
||||
o.User = helpers.OverrideWithString(o.User, other.User)
|
||||
o.Password = helpers.OverrideWithString(o.Password, other.Password)
|
||||
o.ConfFile = helpers.OverrideWithStringPtr(o.ConfFile, other.ConfFile)
|
||||
o.Ciphers = helpers.OverrideWithStringSlice(o.Ciphers, other.Ciphers)
|
||||
o.Auth = helpers.OverrideWithStringPtr(o.Auth, other.Auth)
|
||||
o.ClientCrt = helpers.OverrideWithStringPtr(o.ClientCrt, other.ClientCrt)
|
||||
o.ClientKey = helpers.OverrideWithStringPtr(o.ClientKey, other.ClientKey)
|
||||
o.PIAEncPreset = helpers.OverrideWithStringPtr(o.PIAEncPreset, other.PIAEncPreset)
|
||||
o.IPv6 = helpers.OverrideWithBool(o.IPv6, other.IPv6)
|
||||
o.MSSFix = helpers.OverrideWithUint16(o.MSSFix, other.MSSFix)
|
||||
o.Interface = helpers.OverrideWithString(o.Interface, other.Interface)
|
||||
o.Root = helpers.OverrideWithBool(o.Root, other.Root)
|
||||
o.ProcUser = helpers.OverrideWithString(o.ProcUser, other.ProcUser)
|
||||
o.Verbosity = helpers.OverrideWithInt(o.Verbosity, other.Verbosity)
|
||||
o.Flags = helpers.OverrideWithStringSlice(o.Flags, other.Flags)
|
||||
}
|
||||
|
||||
func (o *OpenVPN) setDefaults(vpnProvider string) {
|
||||
o.Version = helpers.DefaultString(o.Version, constants.Openvpn25)
|
||||
if vpnProvider == constants.Mullvad {
|
||||
o.Password = "m"
|
||||
}
|
||||
|
||||
o.ConfFile = helpers.DefaultStringPtr(o.ConfFile, "")
|
||||
o.Auth = helpers.DefaultStringPtr(o.Auth, "")
|
||||
o.ClientCrt = helpers.DefaultStringPtr(o.ClientCrt, "")
|
||||
o.ClientKey = helpers.DefaultStringPtr(o.ClientKey, "")
|
||||
|
||||
var defaultEncPreset string
|
||||
if vpnProvider == constants.PrivateInternetAccess {
|
||||
defaultEncPreset = constants.PIAEncryptionPresetStrong
|
||||
}
|
||||
o.PIAEncPreset = helpers.DefaultStringPtr(o.PIAEncPreset, defaultEncPreset)
|
||||
|
||||
o.IPv6 = helpers.DefaultBool(o.IPv6, false)
|
||||
o.MSSFix = helpers.DefaultUint16(o.MSSFix, 0)
|
||||
o.Interface = helpers.DefaultString(o.Interface, "tun0")
|
||||
o.Root = helpers.DefaultBool(o.Root, true)
|
||||
o.ProcUser = helpers.DefaultString(o.ProcUser, "root")
|
||||
o.Verbosity = helpers.DefaultInt(o.Verbosity, 1)
|
||||
}
|
||||
|
||||
func (o OpenVPN) String() string {
|
||||
return o.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (o OpenVPN) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("OpenVPN server selection settings:")
|
||||
node.Appendf("OpenVPN version: %s", o.Version)
|
||||
node.Appendf("User: %s", helpers.ObfuscatePassword(o.User))
|
||||
node.Appendf("Password: %s", helpers.ObfuscatePassword(o.Password))
|
||||
|
||||
if *o.ConfFile != "" {
|
||||
node.Appendf("Custom configuration file: %s", *o.ConfFile)
|
||||
}
|
||||
|
||||
if len(o.Ciphers) > 0 {
|
||||
node.Appendf("Ciphers: %s", o.Ciphers)
|
||||
}
|
||||
|
||||
if *o.Auth != "" {
|
||||
node.Appendf("Auth: %s", *o.Auth)
|
||||
}
|
||||
|
||||
if *o.ClientCrt != "" {
|
||||
node.Appendf("Client crt: %s", helpers.ObfuscateData(*o.ClientCrt))
|
||||
}
|
||||
|
||||
if *o.ClientKey != "" {
|
||||
node.Appendf("Client key: %s", helpers.ObfuscateData(*o.ClientKey))
|
||||
}
|
||||
|
||||
if *o.PIAEncPreset != "" {
|
||||
node.Appendf("Private Internet Access encryption preset: %s", *o.PIAEncPreset)
|
||||
}
|
||||
|
||||
node.Appendf("Tunnel IPv6: %s", helpers.BoolPtrToYesNo(o.IPv6))
|
||||
|
||||
if *o.MSSFix > 0 {
|
||||
node.Appendf("MSS Fix: %d", *o.MSSFix)
|
||||
}
|
||||
|
||||
if o.Interface != "" {
|
||||
node.Appendf("Network interface: %s", o.Interface)
|
||||
}
|
||||
|
||||
processUser := "root"
|
||||
if !*o.Root {
|
||||
processUser = "some non root user" // TODO
|
||||
if o.ProcUser != "" {
|
||||
processUser = o.ProcUser
|
||||
}
|
||||
}
|
||||
node.Appendf("Run OpenVPN as: %s", processUser)
|
||||
|
||||
node.Appendf("Verbosity level: %d", *o.Verbosity)
|
||||
|
||||
if len(o.Flags) > 0 {
|
||||
node.Appendf("Flags: %s", o.Flags)
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
// WithDefaults is a shorthand using setDefaults.
|
||||
// It's used in unit tests in other packages.
|
||||
func (o OpenVPN) WithDefaults(provider string) OpenVPN {
|
||||
o.setDefaults(provider)
|
||||
return o
|
||||
}
|
||||
170
internal/configuration/settings/openvpnselection.go
Normal file
170
internal/configuration/settings/openvpnselection.go
Normal file
@@ -0,0 +1,170 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
type OpenVPNSelection struct {
|
||||
// ConfFile is the custom configuration file path.
|
||||
// It can be set to an empty string to indicate to
|
||||
// NOT use a custom configuration file.
|
||||
// It cannot be nil in the internal state.
|
||||
ConfFile *string
|
||||
// TCP is true if the OpenVPN protocol is TCP,
|
||||
// and false for UDP.
|
||||
// It cannot be nil in the internal state.
|
||||
TCP *bool
|
||||
// CustomPort is the OpenVPN server endpoint port.
|
||||
// It can be set to 0 to indicate no custom port should
|
||||
// be used. It cannot be nil in the internal state.
|
||||
CustomPort *uint16 // HideMyAss, Mullvad, PIA, ProtonVPN, WeVPN, Windscribe
|
||||
// PIAEncPreset is the encryption preset for
|
||||
// Private Internet Access. It can be set to an
|
||||
// empty string for other providers.
|
||||
PIAEncPreset *string
|
||||
}
|
||||
|
||||
func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
|
||||
// Validate ConfFile
|
||||
if confFile := *o.ConfFile; confFile != "" {
|
||||
err := helpers.FileExists(confFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %s", ErrOpenVPNConfigFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate TCP
|
||||
if *o.TCP && helpers.IsOneOf(vpnProvider,
|
||||
constants.Perfectprivacy,
|
||||
constants.Privado,
|
||||
constants.Vyprvpn,
|
||||
) {
|
||||
return fmt.Errorf("%w: for VPN service provider %s",
|
||||
ErrOpenVPNTCPNotSupported, vpnProvider)
|
||||
}
|
||||
|
||||
// Validate CustomPort
|
||||
if *o.CustomPort != 0 {
|
||||
switch vpnProvider {
|
||||
// no restriction on port
|
||||
case constants.Cyberghost, constants.HideMyAss,
|
||||
constants.PrivateInternetAccess, constants.Privatevpn,
|
||||
constants.Protonvpn, constants.Torguard:
|
||||
// no custom port allowed
|
||||
case constants.Expressvpn, constants.Fastestvpn,
|
||||
constants.Ipvanish, constants.Nordvpn,
|
||||
constants.Privado, constants.Purevpn,
|
||||
constants.Surfshark, constants.VPNUnlimited,
|
||||
constants.Vyprvpn:
|
||||
return fmt.Errorf("%w: for VPN service provider %s",
|
||||
ErrOpenVPNCustomPortNotAllowed, vpnProvider)
|
||||
default:
|
||||
var allowedTCP, allowedUDP []uint16
|
||||
switch vpnProvider {
|
||||
case constants.Ivpn:
|
||||
allowedTCP = []uint16{80, 443, 1143}
|
||||
allowedUDP = []uint16{53, 1194, 2049, 2050}
|
||||
case constants.Mullvad:
|
||||
allowedTCP = []uint16{80, 443, 1401}
|
||||
allowedUDP = []uint16{53, 1194, 1195, 1196, 1197, 1300, 1301, 1302, 1303, 1400}
|
||||
case constants.Perfectprivacy:
|
||||
allowedTCP = []uint16{44, 443, 4433}
|
||||
allowedUDP = []uint16{44, 443, 4433}
|
||||
case constants.Wevpn:
|
||||
allowedTCP = []uint16{53, 1195, 1199, 2018}
|
||||
allowedUDP = []uint16{80, 1194, 1198}
|
||||
case constants.Windscribe:
|
||||
allowedTCP = []uint16{21, 22, 80, 123, 143, 443, 587, 1194, 3306, 8080, 54783}
|
||||
allowedUDP = []uint16{53, 80, 123, 443, 1194, 54783}
|
||||
}
|
||||
|
||||
if *o.TCP && !helpers.Uint16IsOneOf(*o.CustomPort, allowedTCP) {
|
||||
return fmt.Errorf("%w: %d for VPN service provider %s; %s",
|
||||
ErrOpenVPNCustomPortNotAllowed, o.CustomPort, vpnProvider,
|
||||
helpers.PortChoicesOrString(allowedTCP))
|
||||
} else if !*o.TCP && !helpers.Uint16IsOneOf(*o.CustomPort, allowedUDP) {
|
||||
return fmt.Errorf("%w: %d for VPN service provider %s; %s",
|
||||
ErrOpenVPNCustomPortNotAllowed, o.CustomPort, vpnProvider,
|
||||
helpers.PortChoicesOrString(allowedUDP))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate EncPreset
|
||||
if vpnProvider == constants.PrivateInternetAccess {
|
||||
validEncryptionPresets := []string{
|
||||
constants.PIAEncryptionPresetNone,
|
||||
constants.PIAEncryptionPresetNormal,
|
||||
constants.PIACertificateStrong,
|
||||
}
|
||||
if !helpers.IsOneOf(*o.PIAEncPreset, validEncryptionPresets...) {
|
||||
return fmt.Errorf("%w: %s; valid presets are %s",
|
||||
ErrOpenVPNEncryptionPresetNotValid, *o.PIAEncPreset,
|
||||
helpers.ChoicesOrString(validEncryptionPresets))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *OpenVPNSelection) copy() (copied OpenVPNSelection) {
|
||||
return OpenVPNSelection{
|
||||
ConfFile: helpers.CopyStringPtr(o.ConfFile),
|
||||
TCP: helpers.CopyBoolPtr(o.TCP),
|
||||
CustomPort: helpers.CopyUint16Ptr(o.CustomPort),
|
||||
PIAEncPreset: helpers.CopyStringPtr(o.PIAEncPreset),
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OpenVPNSelection) mergeWith(other OpenVPNSelection) {
|
||||
o.ConfFile = helpers.MergeWithStringPtr(o.ConfFile, other.ConfFile)
|
||||
o.TCP = helpers.MergeWithBool(o.TCP, other.TCP)
|
||||
o.CustomPort = helpers.MergeWithUint16(o.CustomPort, other.CustomPort)
|
||||
o.PIAEncPreset = helpers.MergeWithStringPtr(o.PIAEncPreset, other.PIAEncPreset)
|
||||
}
|
||||
|
||||
func (o *OpenVPNSelection) overrideWith(other OpenVPNSelection) {
|
||||
o.ConfFile = helpers.OverrideWithStringPtr(o.ConfFile, other.ConfFile)
|
||||
o.TCP = helpers.OverrideWithBool(o.TCP, other.TCP)
|
||||
o.CustomPort = helpers.OverrideWithUint16(o.CustomPort, other.CustomPort)
|
||||
o.PIAEncPreset = helpers.OverrideWithStringPtr(o.PIAEncPreset, other.PIAEncPreset)
|
||||
}
|
||||
|
||||
func (o *OpenVPNSelection) setDefaults(vpnProvider string) {
|
||||
o.ConfFile = helpers.DefaultStringPtr(o.ConfFile, "")
|
||||
o.TCP = helpers.DefaultBool(o.TCP, false)
|
||||
o.CustomPort = helpers.DefaultUint16(o.CustomPort, 0)
|
||||
|
||||
var defaultEncPreset string
|
||||
if vpnProvider == constants.PrivateInternetAccess {
|
||||
defaultEncPreset = constants.PIAEncryptionPresetStrong
|
||||
}
|
||||
o.PIAEncPreset = helpers.DefaultStringPtr(o.PIAEncPreset, defaultEncPreset)
|
||||
}
|
||||
|
||||
func (o OpenVPNSelection) String() string {
|
||||
return o.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (o OpenVPNSelection) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("OpenVPN server selection settings:")
|
||||
node.Appendf("Protocol: %s", helpers.TCPPtrToString(o.TCP))
|
||||
|
||||
if *o.CustomPort != 0 {
|
||||
node.Appendf("Custom port: %d", *o.CustomPort)
|
||||
}
|
||||
|
||||
if *o.PIAEncPreset != "" {
|
||||
node.Appendf("Private Internet Access encryption preset: %s", *o.PIAEncPreset)
|
||||
}
|
||||
|
||||
if *o.ConfFile != "" {
|
||||
node.Appendf("Custom configuration file: %s", *o.ConfFile)
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
89
internal/configuration/settings/portforward.go
Normal file
89
internal/configuration/settings/portforward.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
// PortForwarding contains settings for port forwarding.
|
||||
type PortForwarding struct {
|
||||
// Enabled is true if port forwarding should be activated.
|
||||
// It cannot be nil for the internal state.
|
||||
Enabled *bool
|
||||
// Filepath is the port forwarding status file path
|
||||
// to use. It can be the empty string to indicate not
|
||||
// to write to a file. It cannot be nil for the
|
||||
// internal state
|
||||
Filepath *string
|
||||
}
|
||||
|
||||
func (p PortForwarding) validate(vpnProvider string) (err error) {
|
||||
if !*p.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate Enabled
|
||||
validProviders := []string{constants.PrivateInternetAccess}
|
||||
if !helpers.IsOneOf(vpnProvider, validProviders...) {
|
||||
return fmt.Errorf("%w: for provider %s, it is only available for %s",
|
||||
ErrPortForwardingEnabled, vpnProvider, strings.Join(validProviders, ", "))
|
||||
}
|
||||
|
||||
// Validate Filepath
|
||||
if *p.Filepath != "" { // optional
|
||||
_, err := filepath.Abs(*p.Filepath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %s", ErrPortForwardingFilepathNotValid, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PortForwarding) copy() (copied PortForwarding) {
|
||||
return PortForwarding{
|
||||
Enabled: helpers.CopyBoolPtr(p.Enabled),
|
||||
Filepath: helpers.CopyStringPtr(p.Filepath),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PortForwarding) mergeWith(other PortForwarding) {
|
||||
p.Enabled = helpers.MergeWithBool(p.Enabled, other.Enabled)
|
||||
p.Filepath = helpers.MergeWithStringPtr(p.Filepath, other.Filepath)
|
||||
}
|
||||
|
||||
func (p *PortForwarding) overrideWith(other PortForwarding) {
|
||||
p.Enabled = helpers.OverrideWithBool(p.Enabled, other.Enabled)
|
||||
p.Filepath = helpers.OverrideWithStringPtr(p.Filepath, other.Filepath)
|
||||
}
|
||||
|
||||
func (p *PortForwarding) setDefaults() {
|
||||
p.Enabled = helpers.DefaultBool(p.Enabled, false)
|
||||
p.Filepath = helpers.DefaultStringPtr(p.Filepath, "/tmp/gluetun/forwarded_port")
|
||||
}
|
||||
|
||||
func (p PortForwarding) String() string {
|
||||
return p.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (p PortForwarding) toLinesNode() (node *gotree.Node) {
|
||||
if !*p.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
node = gotree.New("Automatic port forwarding settings:")
|
||||
node.Appendf("Enabled: yes")
|
||||
|
||||
filepath := *p.Filepath
|
||||
if filepath == "" {
|
||||
filepath = "[not set]"
|
||||
}
|
||||
node.Appendf("Forwarded port file path: %s", filepath)
|
||||
|
||||
return node
|
||||
}
|
||||
19
internal/configuration/settings/portforward_test.go
Normal file
19
internal/configuration/settings/portforward_test.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_PortForwarding_String(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
settings := PortForwarding{
|
||||
Enabled: boolPtr(false),
|
||||
}
|
||||
|
||||
s := settings.String()
|
||||
|
||||
assert.Empty(t, s)
|
||||
}
|
||||
92
internal/configuration/settings/provider.go
Normal file
92
internal/configuration/settings/provider.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
// Provider contains settings specific to a VPN provider.
|
||||
type Provider struct {
|
||||
// Name is the VPN service provider name.
|
||||
// It cannot be nil in the internal state.
|
||||
Name *string
|
||||
// ServerSelection is the settings to
|
||||
// select the VPN server.
|
||||
ServerSelection ServerSelection
|
||||
// PortForwarding is the settings about port forwarding.
|
||||
PortForwarding PortForwarding
|
||||
}
|
||||
|
||||
func (p Provider) validate(vpnType string, allServers models.AllServers) (err error) {
|
||||
// Validate Name
|
||||
var validNames []string
|
||||
if vpnType == constants.OpenVPN {
|
||||
validNames = constants.AllProviders()
|
||||
validNames = append(validNames, "pia") // Retro-compatibility
|
||||
} else { // Wireguard
|
||||
validNames = []string{
|
||||
constants.Custom,
|
||||
constants.Ivpn,
|
||||
constants.Mullvad,
|
||||
constants.Windscribe,
|
||||
}
|
||||
}
|
||||
if !helpers.IsOneOf(*p.Name, validNames...) {
|
||||
return fmt.Errorf("%w: %q can only be one of %s",
|
||||
ErrVPNProviderNameNotValid, *p.Name, helpers.ChoicesOrString(validNames))
|
||||
}
|
||||
|
||||
err = p.ServerSelection.validate(*p.Name, allServers)
|
||||
if err != nil {
|
||||
return fmt.Errorf("server selection settings validation failed: %w", err)
|
||||
}
|
||||
|
||||
err = p.PortForwarding.validate(*p.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("port forwarding settings validation failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) copy() (copied Provider) {
|
||||
return Provider{
|
||||
Name: helpers.CopyStringPtr(p.Name),
|
||||
ServerSelection: p.ServerSelection.copy(),
|
||||
PortForwarding: p.PortForwarding.copy(),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) mergeWith(other Provider) {
|
||||
p.Name = helpers.MergeWithStringPtr(p.Name, other.Name)
|
||||
p.ServerSelection.mergeWith(other.ServerSelection)
|
||||
p.PortForwarding.mergeWith(other.PortForwarding)
|
||||
}
|
||||
|
||||
func (p *Provider) overrideWith(other Provider) {
|
||||
p.Name = helpers.OverrideWithStringPtr(p.Name, other.Name)
|
||||
p.ServerSelection.overrideWith(other.ServerSelection)
|
||||
p.PortForwarding.overrideWith(other.PortForwarding)
|
||||
}
|
||||
|
||||
func (p *Provider) setDefaults() {
|
||||
p.Name = helpers.DefaultStringPtr(p.Name, constants.PrivateInternetAccess)
|
||||
p.ServerSelection.setDefaults(*p.Name)
|
||||
p.PortForwarding.setDefaults()
|
||||
}
|
||||
|
||||
func (p Provider) String() string {
|
||||
return p.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (p Provider) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("VPN provider settings:")
|
||||
node.Appendf("Name: %s", *p.Name)
|
||||
node.AppendNode(p.ServerSelection.toLinesNode())
|
||||
node.AppendNode(p.PortForwarding.toLinesNode())
|
||||
return node
|
||||
}
|
||||
89
internal/configuration/settings/publicip.go
Normal file
89
internal/configuration/settings/publicip.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
// PublicIP contains settings for port forwarding.
|
||||
type PublicIP struct {
|
||||
// Period is the period to get the public IP address.
|
||||
// It can be set to 0 to disable periodic checking.
|
||||
// It cannot be nil for the internal state.
|
||||
// TODO change to value and add enabled field
|
||||
Period *time.Duration
|
||||
// IPFilepath is the public IP address status file path
|
||||
// to use. It can be the empty string to indicate not
|
||||
// to write to a file. It cannot be nil for the
|
||||
// internal state
|
||||
IPFilepath *string
|
||||
}
|
||||
|
||||
func (p PublicIP) validate() (err error) {
|
||||
const minPeriod = 5 * time.Second
|
||||
if *p.Period < minPeriod {
|
||||
return fmt.Errorf("%w: %s must be at least %s",
|
||||
ErrPublicIPPeriodTooShort, p.Period, minPeriod)
|
||||
}
|
||||
|
||||
if *p.IPFilepath != "" { // optional
|
||||
_, err := filepath.Abs(*p.IPFilepath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %s", ErrPublicIPFilepathNotValid, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PublicIP) copy() (copied PublicIP) {
|
||||
return PublicIP{
|
||||
Period: helpers.CopyDurationPtr(p.Period),
|
||||
IPFilepath: helpers.CopyStringPtr(p.IPFilepath),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PublicIP) mergeWith(other PublicIP) {
|
||||
p.Period = helpers.MergeWithDuration(p.Period, other.Period)
|
||||
p.IPFilepath = helpers.MergeWithStringPtr(p.IPFilepath, other.IPFilepath)
|
||||
}
|
||||
|
||||
func (p *PublicIP) overrideWith(other PublicIP) {
|
||||
p.Period = helpers.OverrideWithDuration(p.Period, other.Period)
|
||||
p.IPFilepath = helpers.OverrideWithStringPtr(p.IPFilepath, other.IPFilepath)
|
||||
}
|
||||
|
||||
func (p *PublicIP) setDefaults() {
|
||||
const defaultPeriod = 12 * time.Hour
|
||||
p.Period = helpers.DefaultDuration(p.Period, defaultPeriod)
|
||||
p.IPFilepath = helpers.DefaultStringPtr(p.IPFilepath, "/tmp/gluetun/ip")
|
||||
}
|
||||
|
||||
func (p PublicIP) String() string {
|
||||
return p.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (p PublicIP) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("Public IP settings:")
|
||||
|
||||
if *p.Period == 0 {
|
||||
node.Appendf("Enabled: no")
|
||||
return node
|
||||
}
|
||||
|
||||
updatePeriod := "disabled"
|
||||
if *p.Period > 0 {
|
||||
updatePeriod = "every " + p.Period.String()
|
||||
}
|
||||
node.Appendf("Fetching: %s", updatePeriod)
|
||||
|
||||
if *p.IPFilepath != "" {
|
||||
node.Appendf("IP file path: %s", *p.IPFilepath)
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
71
internal/configuration/settings/server.go
Normal file
71
internal/configuration/settings/server.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
// ControlServer contains settings to customize the control server operation.
|
||||
type ControlServer struct {
|
||||
// Port is the listening port to use.
|
||||
// It can be set to 0 to bind to a random port.
|
||||
// It cannot be nil in the internal state.
|
||||
// TODO change to address
|
||||
Port *uint16
|
||||
// Log can be true or false to enable logging on requests.
|
||||
// It cannot be nil in the internal state.
|
||||
Log *bool
|
||||
}
|
||||
|
||||
func (c ControlServer) validate() (err error) {
|
||||
uid := os.Getuid()
|
||||
const maxPrivilegedPort uint16 = 1023
|
||||
if uid != 0 && *c.Port <= maxPrivilegedPort {
|
||||
return fmt.Errorf("%w: %d when running with user ID %d",
|
||||
ErrControlServerPrivilegedPort, *c.Port, uid)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ControlServer) copy() (copied ControlServer) {
|
||||
return ControlServer{
|
||||
Port: helpers.CopyUint16Ptr(c.Port),
|
||||
Log: helpers.CopyBoolPtr(c.Log),
|
||||
}
|
||||
}
|
||||
|
||||
// mergeWith merges the other settings into any
|
||||
// unset field of the receiver settings object.
|
||||
func (c *ControlServer) mergeWith(other ControlServer) {
|
||||
c.Port = helpers.MergeWithUint16(c.Port, other.Port)
|
||||
c.Log = helpers.MergeWithBool(c.Log, other.Log)
|
||||
}
|
||||
|
||||
// overrideWith overrides fields of the receiver
|
||||
// settings object with any field set in the other
|
||||
// settings.
|
||||
func (c *ControlServer) overrideWith(other ControlServer) {
|
||||
c.Port = helpers.MergeWithUint16(c.Port, other.Port)
|
||||
c.Log = helpers.MergeWithBool(c.Log, other.Log)
|
||||
}
|
||||
|
||||
func (c *ControlServer) setDefaults() {
|
||||
const defaultPort = 8000
|
||||
c.Port = helpers.DefaultUint16(c.Port, defaultPort)
|
||||
c.Log = helpers.DefaultBool(c.Log, true)
|
||||
}
|
||||
|
||||
func (c ControlServer) String() string {
|
||||
return c.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (c ControlServer) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("Control server settings:")
|
||||
node.Appendf("Listening port: %d", *c.Port)
|
||||
node.Appendf("Logging: %s", helpers.BoolPtrToYesNo(c.Log))
|
||||
return node
|
||||
}
|
||||
386
internal/configuration/settings/serverselection.go
Normal file
386
internal/configuration/settings/serverselection.go
Normal file
@@ -0,0 +1,386 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
type ServerSelection struct { //nolint:maligned
|
||||
// VPN is the VPN type which can be 'openvpn'
|
||||
// or 'wireguard'. It cannot be the empty string
|
||||
// in the internal state.
|
||||
VPN string
|
||||
// TargetIP is the server endpoint IP address to use.
|
||||
// It will override any IP address from the picked
|
||||
// built-in server. It cannot be nil in the internal
|
||||
// state, and can be set to an empty net.IP{} to indicate
|
||||
// there is not target IP address to use.
|
||||
TargetIP net.IP
|
||||
// Counties is the list of countries to filter VPN servers with.
|
||||
Countries []string
|
||||
// Regions is the list of regions to filter VPN servers with.
|
||||
Regions []string
|
||||
// Cities is the list of cities to filter VPN servers with.
|
||||
Cities []string
|
||||
// ISPs is the list of ISP names to filter VPN servers with.
|
||||
ISPs []string
|
||||
// Names is the list of server names to filter VPN servers with.
|
||||
Names []string
|
||||
// Numbers is the list of server numbers to filter VPN servers with.
|
||||
Numbers []uint16
|
||||
// Hostnames is the list of hostnames to filter VPN servers with.
|
||||
Hostnames []string
|
||||
// OwnedOnly is true if only VPN provider owned servers
|
||||
// should be filtered. This is used with Mullvad.
|
||||
OwnedOnly *bool
|
||||
// FreeOnly is true if only free VPN servers
|
||||
// should be filtered. This is used with ProtonVPN.
|
||||
FreeOnly *bool
|
||||
// FreeOnly is true if only free VPN servers
|
||||
// should be filtered. This is used with ProtonVPN.
|
||||
StreamOnly *bool
|
||||
// MultiHopOnly is true if only multihop VPN servers
|
||||
// should be filtered. This is used with Surfshark.
|
||||
MultiHopOnly *bool
|
||||
|
||||
// OpenVPN contains settings to select OpenVPN servers
|
||||
// and the final connection.
|
||||
OpenVPN OpenVPNSelection
|
||||
// Wireguard contains settings to select Wireguard servers
|
||||
// and the final connection.
|
||||
Wireguard WireguardSelection
|
||||
}
|
||||
|
||||
func (ss *ServerSelection) validate(vpnServiceProvider string,
|
||||
allServers models.AllServers) (err error) {
|
||||
switch ss.VPN {
|
||||
case constants.OpenVPN, constants.Wireguard:
|
||||
default:
|
||||
return fmt.Errorf("%w: %s", ErrVPNTypeNotValid, ss.VPN)
|
||||
}
|
||||
|
||||
var countryChoices, regionChoices, cityChoices,
|
||||
ispChoices, nameChoices, hostnameChoices []string
|
||||
switch vpnServiceProvider {
|
||||
case constants.Cyberghost:
|
||||
servers := allServers.GetCyberghost()
|
||||
countryChoices = constants.CyberghostCountryChoices(servers)
|
||||
hostnameChoices = constants.CyberghostHostnameChoices(servers)
|
||||
case constants.Expressvpn:
|
||||
servers := allServers.GetExpressvpn()
|
||||
countryChoices = constants.ExpressvpnCountriesChoices(servers)
|
||||
cityChoices = constants.ExpressvpnCityChoices(servers)
|
||||
hostnameChoices = constants.ExpressvpnHostnameChoices(servers)
|
||||
case constants.Fastestvpn:
|
||||
servers := allServers.GetFastestvpn()
|
||||
countryChoices = constants.FastestvpnCountriesChoices(servers)
|
||||
hostnameChoices = constants.FastestvpnHostnameChoices(servers)
|
||||
case constants.HideMyAss:
|
||||
servers := allServers.GetHideMyAss()
|
||||
countryChoices = constants.HideMyAssCountryChoices(servers)
|
||||
regionChoices = constants.HideMyAssRegionChoices(servers)
|
||||
cityChoices = constants.HideMyAssCityChoices(servers)
|
||||
hostnameChoices = constants.HideMyAssHostnameChoices(servers)
|
||||
case constants.Ipvanish:
|
||||
servers := allServers.GetIpvanish()
|
||||
countryChoices = constants.IpvanishCountryChoices(servers)
|
||||
cityChoices = constants.IpvanishCityChoices(servers)
|
||||
hostnameChoices = constants.IpvanishHostnameChoices(servers)
|
||||
case constants.Ivpn:
|
||||
servers := allServers.GetIvpn()
|
||||
countryChoices = constants.IvpnCountryChoices(servers)
|
||||
cityChoices = constants.IvpnCityChoices(servers)
|
||||
ispChoices = constants.IvpnISPChoices(servers)
|
||||
hostnameChoices = constants.IvpnHostnameChoices(servers)
|
||||
case constants.Mullvad:
|
||||
servers := allServers.GetMullvad()
|
||||
countryChoices = constants.MullvadCountryChoices(servers)
|
||||
cityChoices = constants.MullvadCityChoices(servers)
|
||||
ispChoices = constants.MullvadISPChoices(servers)
|
||||
hostnameChoices = constants.MullvadHostnameChoices(servers)
|
||||
case constants.Nordvpn:
|
||||
servers := allServers.GetNordvpn()
|
||||
regionChoices = constants.NordvpnRegionChoices(servers)
|
||||
hostnameChoices = constants.NordvpnHostnameChoices(servers)
|
||||
case constants.Perfectprivacy:
|
||||
servers := allServers.GetPerfectprivacy()
|
||||
cityChoices = constants.PerfectprivacyCityChoices(servers)
|
||||
case constants.Privado:
|
||||
servers := allServers.GetPrivado()
|
||||
countryChoices = constants.PrivadoCountryChoices(servers)
|
||||
regionChoices = constants.PrivadoRegionChoices(servers)
|
||||
cityChoices = constants.PrivadoCityChoices(servers)
|
||||
hostnameChoices = constants.PrivadoHostnameChoices(servers)
|
||||
case constants.PrivateInternetAccess:
|
||||
servers := allServers.GetPia()
|
||||
regionChoices = constants.PIAGeoChoices(servers)
|
||||
hostnameChoices = constants.PIAHostnameChoices(servers)
|
||||
nameChoices = constants.PIANameChoices(servers)
|
||||
case constants.Privatevpn:
|
||||
servers := allServers.GetPrivatevpn()
|
||||
countryChoices = constants.PrivatevpnCountryChoices(servers)
|
||||
cityChoices = constants.PrivatevpnCityChoices(servers)
|
||||
hostnameChoices = constants.PrivatevpnHostnameChoices(servers)
|
||||
case constants.Protonvpn:
|
||||
servers := allServers.GetProtonvpn()
|
||||
countryChoices = constants.ProtonvpnCountryChoices(servers)
|
||||
regionChoices = constants.ProtonvpnRegionChoices(servers)
|
||||
cityChoices = constants.ProtonvpnCityChoices(servers)
|
||||
nameChoices = constants.ProtonvpnNameChoices(servers)
|
||||
hostnameChoices = constants.ProtonvpnHostnameChoices(servers)
|
||||
case constants.Purevpn:
|
||||
servers := allServers.GetPurevpn()
|
||||
countryChoices = constants.PurevpnCountryChoices(servers)
|
||||
regionChoices = constants.PurevpnRegionChoices(servers)
|
||||
cityChoices = constants.PurevpnCityChoices(servers)
|
||||
hostnameChoices = constants.PurevpnHostnameChoices(servers)
|
||||
case constants.Surfshark:
|
||||
servers := allServers.GetSurfshark()
|
||||
countryChoices = constants.SurfsharkCountryChoices(servers)
|
||||
cityChoices = constants.SurfsharkCityChoices(servers)
|
||||
hostnameChoices = constants.SurfsharkHostnameChoices(servers)
|
||||
regionChoices = constants.SurfsharkRegionChoices(servers)
|
||||
// TODO v4 remove
|
||||
regionChoices = append(regionChoices, constants.SurfsharkRetroLocChoices(servers)...)
|
||||
if err := helpers.AreAllOneOf(ss.Regions, regionChoices); err != nil {
|
||||
return fmt.Errorf("%w: %s", ErrRegionNotValid, err)
|
||||
}
|
||||
// Retro compatibility
|
||||
// TODO remove in v4
|
||||
*ss = surfsharkRetroRegion(*ss)
|
||||
case constants.Torguard:
|
||||
servers := allServers.GetTorguard()
|
||||
countryChoices = constants.TorguardCountryChoices(servers)
|
||||
cityChoices = constants.TorguardCityChoices(servers)
|
||||
hostnameChoices = constants.TorguardHostnameChoices(servers)
|
||||
case constants.VPNUnlimited:
|
||||
servers := allServers.GetVPNUnlimited()
|
||||
countryChoices = constants.VPNUnlimitedCountryChoices(servers)
|
||||
cityChoices = constants.VPNUnlimitedCityChoices(servers)
|
||||
hostnameChoices = constants.VPNUnlimitedHostnameChoices(servers)
|
||||
case constants.Vyprvpn:
|
||||
servers := allServers.GetVyprvpn()
|
||||
regionChoices = constants.VyprvpnRegionChoices(servers)
|
||||
case constants.Wevpn:
|
||||
servers := allServers.GetWevpn()
|
||||
cityChoices = constants.WevpnCityChoices(servers)
|
||||
hostnameChoices = constants.WevpnHostnameChoices(servers)
|
||||
case constants.Windscribe:
|
||||
servers := allServers.GetWindscribe()
|
||||
regionChoices = constants.WindscribeRegionChoices(servers)
|
||||
cityChoices = constants.WindscribeCityChoices(servers)
|
||||
hostnameChoices = constants.WindscribeHostnameChoices(servers)
|
||||
default:
|
||||
return fmt.Errorf("%w: %s", ErrVPNProviderNameNotValid, ss.VPN)
|
||||
}
|
||||
|
||||
err = validateServerFilters(*ss, countryChoices, regionChoices, cityChoices,
|
||||
ispChoices, nameChoices, hostnameChoices)
|
||||
if err != nil {
|
||||
return err // already wrapped error
|
||||
}
|
||||
|
||||
err = ss.OpenVPN.validate(vpnServiceProvider)
|
||||
if err != nil {
|
||||
return fmt.Errorf("OpenVPN server selection settings validation failed: %w", err)
|
||||
}
|
||||
|
||||
err = ss.Wireguard.validate(vpnServiceProvider)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Wireguard server selection settings validation failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateServerFilters validates filters against the choices given as arguments.
|
||||
// Set an argument to nil to pass the check for a particular filter.
|
||||
func validateServerFilters(settings ServerSelection,
|
||||
countryChoices, regionChoices, cityChoices, ispChoices,
|
||||
nameChoices, hostnameChoices []string) (err error) {
|
||||
if countryChoices != nil {
|
||||
if err := helpers.AreAllOneOf(settings.Countries, countryChoices); err != nil {
|
||||
return fmt.Errorf("%w: %s", ErrCountryNotValid, err)
|
||||
}
|
||||
}
|
||||
|
||||
if regionChoices != nil {
|
||||
if err := helpers.AreAllOneOf(settings.Regions, regionChoices); err != nil {
|
||||
return fmt.Errorf("%w: %s", ErrRegionNotValid, err)
|
||||
}
|
||||
}
|
||||
|
||||
if cityChoices != nil {
|
||||
if err := helpers.AreAllOneOf(settings.Cities, cityChoices); err != nil {
|
||||
return fmt.Errorf("%w: %s", ErrCityNotValid, err)
|
||||
}
|
||||
}
|
||||
|
||||
if ispChoices != nil {
|
||||
if err := helpers.AreAllOneOf(settings.ISPs, ispChoices); err != nil {
|
||||
return fmt.Errorf("%w: %s", ErrISPNotValid, err)
|
||||
}
|
||||
}
|
||||
|
||||
if hostnameChoices != nil {
|
||||
if err := helpers.AreAllOneOf(settings.Hostnames, hostnameChoices); err != nil {
|
||||
return fmt.Errorf("%w: %s", ErrHostnameNotValid, err)
|
||||
}
|
||||
}
|
||||
|
||||
if nameChoices != nil {
|
||||
if err := helpers.AreAllOneOf(settings.Names, nameChoices); err != nil {
|
||||
return fmt.Errorf("%w: %s", ErrNameNotValid, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ss *ServerSelection) copy() (copied ServerSelection) {
|
||||
return ServerSelection{
|
||||
VPN: ss.VPN,
|
||||
TargetIP: helpers.CopyIP(ss.TargetIP),
|
||||
Countries: helpers.CopyStringSlice(ss.Countries),
|
||||
Regions: helpers.CopyStringSlice(ss.Regions),
|
||||
Cities: helpers.CopyStringSlice(ss.Cities),
|
||||
ISPs: helpers.CopyStringSlice(ss.ISPs),
|
||||
Hostnames: helpers.CopyStringSlice(ss.Hostnames),
|
||||
Names: helpers.CopyStringSlice(ss.Names),
|
||||
Numbers: helpers.CopyUint16Slice(ss.Numbers),
|
||||
OwnedOnly: helpers.CopyBoolPtr(ss.OwnedOnly),
|
||||
FreeOnly: helpers.CopyBoolPtr(ss.FreeOnly),
|
||||
StreamOnly: helpers.CopyBoolPtr(ss.StreamOnly),
|
||||
MultiHopOnly: helpers.CopyBoolPtr(ss.MultiHopOnly),
|
||||
OpenVPN: ss.OpenVPN.copy(),
|
||||
Wireguard: ss.Wireguard.copy(),
|
||||
}
|
||||
}
|
||||
|
||||
func (ss *ServerSelection) mergeWith(other ServerSelection) {
|
||||
ss.VPN = helpers.MergeWithString(ss.VPN, other.VPN)
|
||||
ss.TargetIP = helpers.MergeWithIP(ss.TargetIP, other.TargetIP)
|
||||
ss.Countries = helpers.MergeStringSlices(ss.Countries, other.Countries)
|
||||
ss.Regions = helpers.MergeStringSlices(ss.Regions, other.Regions)
|
||||
ss.Cities = helpers.MergeStringSlices(ss.Cities, other.Cities)
|
||||
ss.ISPs = helpers.MergeStringSlices(ss.ISPs, other.ISPs)
|
||||
ss.Hostnames = helpers.MergeStringSlices(ss.Hostnames, other.Hostnames)
|
||||
ss.Names = helpers.MergeStringSlices(ss.Hostnames, other.Names)
|
||||
ss.Numbers = helpers.MergeUint16Slices(ss.Numbers, other.Numbers)
|
||||
ss.OwnedOnly = helpers.MergeWithBool(ss.OwnedOnly, other.OwnedOnly)
|
||||
ss.FreeOnly = helpers.MergeWithBool(ss.FreeOnly, other.FreeOnly)
|
||||
ss.StreamOnly = helpers.MergeWithBool(ss.StreamOnly, other.StreamOnly)
|
||||
ss.MultiHopOnly = helpers.MergeWithBool(ss.MultiHopOnly, other.MultiHopOnly)
|
||||
|
||||
ss.OpenVPN.mergeWith(other.OpenVPN)
|
||||
ss.Wireguard.mergeWith(other.Wireguard)
|
||||
}
|
||||
|
||||
func (ss *ServerSelection) overrideWith(other ServerSelection) {
|
||||
ss.VPN = helpers.OverrideWithString(ss.VPN, other.VPN)
|
||||
ss.TargetIP = helpers.OverrideWithIP(ss.TargetIP, other.TargetIP)
|
||||
ss.Countries = helpers.OverrideWithStringSlice(ss.Countries, other.Countries)
|
||||
ss.Regions = helpers.OverrideWithStringSlice(ss.Regions, other.Regions)
|
||||
ss.Cities = helpers.OverrideWithStringSlice(ss.Cities, other.Cities)
|
||||
ss.ISPs = helpers.OverrideWithStringSlice(ss.ISPs, other.ISPs)
|
||||
ss.Hostnames = helpers.OverrideWithStringSlice(ss.Hostnames, other.Hostnames)
|
||||
ss.Names = helpers.OverrideWithStringSlice(ss.Hostnames, other.Names)
|
||||
ss.Numbers = helpers.OverrideWithUint16Slice(ss.Numbers, other.Numbers)
|
||||
ss.OwnedOnly = helpers.OverrideWithBool(ss.OwnedOnly, other.OwnedOnly)
|
||||
ss.FreeOnly = helpers.OverrideWithBool(ss.FreeOnly, other.FreeOnly)
|
||||
ss.StreamOnly = helpers.OverrideWithBool(ss.StreamOnly, other.StreamOnly)
|
||||
ss.MultiHopOnly = helpers.OverrideWithBool(ss.MultiHopOnly, other.MultiHopOnly)
|
||||
ss.OpenVPN.overrideWith(other.OpenVPN)
|
||||
ss.Wireguard.overrideWith(other.Wireguard)
|
||||
}
|
||||
|
||||
func (ss *ServerSelection) setDefaults(vpnProvider string) {
|
||||
ss.VPN = helpers.DefaultString(ss.VPN, constants.OpenVPN)
|
||||
ss.TargetIP = helpers.DefaultIP(ss.TargetIP, net.IP{})
|
||||
ss.OwnedOnly = helpers.DefaultBool(ss.OwnedOnly, false)
|
||||
ss.FreeOnly = helpers.DefaultBool(ss.FreeOnly, false)
|
||||
ss.StreamOnly = helpers.DefaultBool(ss.StreamOnly, false)
|
||||
ss.MultiHopOnly = helpers.DefaultBool(ss.MultiHopOnly, false)
|
||||
ss.OpenVPN.setDefaults(vpnProvider)
|
||||
ss.Wireguard.setDefaults()
|
||||
}
|
||||
|
||||
func (ss ServerSelection) String() string {
|
||||
return ss.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("Server selection settings:")
|
||||
node.Appendf("VPN type: %s", ss.VPN)
|
||||
if len(ss.TargetIP) > 0 {
|
||||
node.Appendf("Target IP address: %s", ss.TargetIP)
|
||||
}
|
||||
|
||||
if len(ss.Countries) > 0 {
|
||||
node.Appendf("Countries: %s", strings.Join(ss.Countries, ", "))
|
||||
}
|
||||
|
||||
if len(ss.Regions) > 0 {
|
||||
node.Appendf("Regions: %s", strings.Join(ss.Regions, ", "))
|
||||
}
|
||||
|
||||
if len(ss.Cities) > 0 {
|
||||
node.Appendf("Cities: %s", strings.Join(ss.Cities, ", "))
|
||||
}
|
||||
|
||||
if len(ss.ISPs) > 0 {
|
||||
node.Appendf("ISPs: %s", strings.Join(ss.ISPs, ", "))
|
||||
}
|
||||
|
||||
if len(ss.Names) > 0 {
|
||||
node.Appendf("Server names: %s", strings.Join(ss.Names, ", "))
|
||||
}
|
||||
|
||||
if len(ss.Numbers) > 0 {
|
||||
numbersNode := node.Appendf("Server numbers:")
|
||||
for _, number := range ss.Numbers {
|
||||
numbersNode.Appendf("%d", number)
|
||||
}
|
||||
}
|
||||
|
||||
if len(ss.Hostnames) > 0 {
|
||||
node.Appendf("Hostnames: %s", strings.Join(ss.Hostnames, ", "))
|
||||
}
|
||||
|
||||
if *ss.OwnedOnly {
|
||||
node.Appendf("Owned only servers: yes")
|
||||
}
|
||||
|
||||
if *ss.FreeOnly {
|
||||
node.Appendf("Free only servers: yes")
|
||||
}
|
||||
|
||||
if *ss.StreamOnly {
|
||||
node.Appendf("Stream only servers: yes")
|
||||
}
|
||||
|
||||
if *ss.MultiHopOnly {
|
||||
node.Appendf("Multi-hop only servers: yes")
|
||||
}
|
||||
|
||||
if ss.VPN == constants.OpenVPN {
|
||||
node.AppendNode(ss.OpenVPN.toLinesNode())
|
||||
} else {
|
||||
node.AppendNode(ss.Wireguard.toLinesNode())
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
// WithDefaults is a shorthand using setDefaults.
|
||||
// It's used in unit tests in other packages.
|
||||
func (ss ServerSelection) WithDefaults(provider string) ServerSelection {
|
||||
ss.setDefaults(provider)
|
||||
return ss
|
||||
}
|
||||
144
internal/configuration/settings/settings.go
Normal file
144
internal/configuration/settings/settings.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
ControlServer ControlServer
|
||||
DNS DNS
|
||||
Firewall Firewall
|
||||
Health Health
|
||||
HTTPProxy HTTPProxy
|
||||
Log Log
|
||||
PublicIP PublicIP
|
||||
Shadowsocks Shadowsocks
|
||||
System System
|
||||
Updater Updater
|
||||
Version Version
|
||||
VPN VPN
|
||||
}
|
||||
|
||||
func (s Settings) Validate(allServers models.AllServers) (err error) {
|
||||
nameToValidation := map[string]func() error{
|
||||
"control server": s.ControlServer.validate,
|
||||
"dns": s.DNS.validate,
|
||||
"firewall": s.Firewall.validate,
|
||||
"health": s.Health.Validate,
|
||||
"http proxy": s.HTTPProxy.validate,
|
||||
"log": s.Log.validate,
|
||||
"public ip check": s.PublicIP.validate,
|
||||
"shadowsocks": s.Shadowsocks.validate,
|
||||
"system": s.System.validate,
|
||||
"updater": s.Updater.Validate,
|
||||
"version": s.Version.validate,
|
||||
"VPN": func() error {
|
||||
return s.VPN.validate(allServers)
|
||||
},
|
||||
}
|
||||
|
||||
for name, validation := range nameToValidation {
|
||||
err = validation()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed validating %s settings: %w", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Settings) copy() (copied Settings) {
|
||||
return Settings{
|
||||
ControlServer: s.ControlServer.copy(),
|
||||
DNS: s.DNS.Copy(),
|
||||
Firewall: s.Firewall.copy(),
|
||||
Health: s.Health.copy(),
|
||||
HTTPProxy: s.HTTPProxy.copy(),
|
||||
Log: s.Log.copy(),
|
||||
PublicIP: s.PublicIP.copy(),
|
||||
Shadowsocks: s.Shadowsocks.copy(),
|
||||
System: s.System.copy(),
|
||||
Updater: s.Updater.copy(),
|
||||
Version: s.Version.copy(),
|
||||
VPN: s.VPN.copy(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Settings) MergeWith(other Settings) {
|
||||
s.ControlServer.mergeWith(other.ControlServer)
|
||||
s.DNS.mergeWith(other.DNS)
|
||||
s.Firewall.mergeWith(other.Firewall)
|
||||
s.Health.MergeWith(other.Health)
|
||||
s.HTTPProxy.mergeWith(other.HTTPProxy)
|
||||
s.Log.mergeWith(other.Log)
|
||||
s.PublicIP.mergeWith(other.PublicIP)
|
||||
s.Shadowsocks.mergeWith(other.Shadowsocks)
|
||||
s.System.mergeWith(other.System)
|
||||
s.Updater.mergeWith(other.Updater)
|
||||
s.Version.mergeWith(other.Version)
|
||||
s.VPN.mergeWith(other.VPN)
|
||||
}
|
||||
|
||||
func (s *Settings) OverrideWith(other Settings,
|
||||
allServers models.AllServers) (err error) {
|
||||
patchedSettings := s.copy()
|
||||
patchedSettings.ControlServer.overrideWith(other.ControlServer)
|
||||
patchedSettings.DNS.overrideWith(other.DNS)
|
||||
patchedSettings.Firewall.overrideWith(other.Firewall)
|
||||
patchedSettings.Health.OverrideWith(other.Health)
|
||||
patchedSettings.HTTPProxy.overrideWith(other.HTTPProxy)
|
||||
patchedSettings.Log.overrideWith(other.Log)
|
||||
patchedSettings.PublicIP.overrideWith(other.PublicIP)
|
||||
patchedSettings.Shadowsocks.overrideWith(other.Shadowsocks)
|
||||
patchedSettings.System.overrideWith(other.System)
|
||||
patchedSettings.Updater.overrideWith(other.Updater)
|
||||
patchedSettings.Version.overrideWith(other.Version)
|
||||
patchedSettings.VPN.overrideWith(other.VPN)
|
||||
err = patchedSettings.Validate(allServers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*s = patchedSettings
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Settings) SetDefaults() {
|
||||
s.ControlServer.setDefaults()
|
||||
s.DNS.setDefaults()
|
||||
s.Firewall.setDefaults()
|
||||
s.Health.SetDefaults()
|
||||
s.HTTPProxy.setDefaults()
|
||||
s.Log.setDefaults()
|
||||
s.PublicIP.setDefaults()
|
||||
s.Shadowsocks.setDefaults()
|
||||
s.System.setDefaults()
|
||||
s.Updater.setDefaults()
|
||||
s.Version.setDefaults()
|
||||
s.VPN.setDefaults()
|
||||
}
|
||||
|
||||
func (s Settings) String() string {
|
||||
return s.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (s Settings) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("Settings summary:")
|
||||
|
||||
node.AppendNode(s.VPN.toLinesNode())
|
||||
node.AppendNode(s.DNS.toLinesNode())
|
||||
node.AppendNode(s.Firewall.toLinesNode())
|
||||
node.AppendNode(s.Log.toLinesNode())
|
||||
node.AppendNode(s.Health.toLinesNode())
|
||||
node.AppendNode(s.Shadowsocks.toLinesNode())
|
||||
node.AppendNode(s.HTTPProxy.toLinesNode())
|
||||
node.AppendNode(s.ControlServer.toLinesNode())
|
||||
node.AppendNode(s.System.toLinesNode())
|
||||
node.AppendNode(s.PublicIP.toLinesNode())
|
||||
node.AppendNode(s.Updater.toLinesNode())
|
||||
node.AppendNode(s.Version.toLinesNode())
|
||||
|
||||
return node
|
||||
}
|
||||
101
internal/configuration/settings/settings_test.go
Normal file
101
internal/configuration/settings/settings_test.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Settings_String(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
withDefaults := func(s Settings) Settings {
|
||||
s.SetDefaults()
|
||||
return s
|
||||
}
|
||||
|
||||
testCases := map[string]struct {
|
||||
settings Settings
|
||||
s string
|
||||
}{
|
||||
"default settings": {
|
||||
settings: withDefaults(Settings{}),
|
||||
s: `Settings summary:
|
||||
├── VPN settings:
|
||||
| ├── VPN provider settings:
|
||||
| | ├── Name: private internet access
|
||||
| | └── Server selection settings:
|
||||
| | ├── VPN type: openvpn
|
||||
| | └── OpenVPN server selection settings:
|
||||
| | ├── Protocol: UDP
|
||||
| | └── Private Internet Access encryption preset: strong
|
||||
| └── OpenVPN server selection settings:
|
||||
| ├── OpenVPN version: 2.5
|
||||
| ├── User: [not set]
|
||||
| ├── Password: [not set]
|
||||
| ├── Private Internet Access encryption preset: strong
|
||||
| ├── Tunnel IPv6: no
|
||||
| ├── Network interface: tun0
|
||||
| ├── Run OpenVPN as: root
|
||||
| └── Verbosity level: 1
|
||||
├── DNS settings:
|
||||
| ├── DNS server address to use: 127.0.0.1
|
||||
| ├── Keep existing nameserver(s): no
|
||||
| └── DNS over TLS settings:
|
||||
| ├── Enabled: yes
|
||||
| ├── Update period: every 24h0m0s
|
||||
| ├── Unbound settings:
|
||||
| | ├── Authoritative servers:
|
||||
| | | └── Cloudflare
|
||||
| | ├── Caching: yes
|
||||
| | ├── IPv6: no
|
||||
| | ├── Verbosity level: 1
|
||||
| | ├── Verbosity details level: 0
|
||||
| | ├── Validation log level: 0
|
||||
| | ├── System user: root
|
||||
| | └── Allowed networks:
|
||||
| | ├── 0.0.0.0/0
|
||||
| | └── ::/0
|
||||
| └── DNS filtering settings:
|
||||
| ├── Block malicious: yes
|
||||
| ├── Block ads: no
|
||||
| └── Block surveillance: yes
|
||||
├── Firewall settings:
|
||||
| └── Enabled: yes
|
||||
├── Log settings:
|
||||
| └── Log level: INFO
|
||||
├── Health settings:
|
||||
| ├── Server listening address: 127.0.0.1:9999
|
||||
| ├── Address to ping: github.com
|
||||
| └── VPN wait durations:
|
||||
| ├── Initial duration: 6s
|
||||
| └── Additional duration: 5s
|
||||
├── Shadowsocks server settings:
|
||||
| └── Enabled: no
|
||||
├── HTTP proxy settings:
|
||||
| └── Enabled: no
|
||||
├── Control server settings:
|
||||
| ├── Listening port: 8000
|
||||
| └── Logging: yes
|
||||
├── OS Alpine settings:
|
||||
| ├── Process UID: 1000
|
||||
| └── Process GID: 1000
|
||||
├── Public IP settings:
|
||||
| ├── Fetching: every 12h0m0s
|
||||
| └── IP file path: /tmp/gluetun/ip
|
||||
└── Version settings:
|
||||
└── Enabled: yes`,
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := testCase.settings.String()
|
||||
|
||||
assert.Equal(t, testCase.s, s)
|
||||
})
|
||||
}
|
||||
}
|
||||
68
internal/configuration/settings/shadowsocks.go
Normal file
68
internal/configuration/settings/shadowsocks.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gotree"
|
||||
"github.com/qdm12/ss-server/pkg/tcpudp"
|
||||
)
|
||||
|
||||
// Shadowsocks contains settings to configure the Shadowsocks server.
|
||||
type Shadowsocks struct {
|
||||
// Enabled is true if the server should be running.
|
||||
// It defaults to false, and cannot be nil in the internal state.
|
||||
Enabled *bool
|
||||
// Settings are settings for the TCP+UDP server.
|
||||
tcpudp.Settings
|
||||
}
|
||||
|
||||
func (s Shadowsocks) validate() (err error) {
|
||||
return s.Settings.Validate()
|
||||
}
|
||||
|
||||
func (s *Shadowsocks) copy() (copied Shadowsocks) {
|
||||
return Shadowsocks{
|
||||
Enabled: helpers.CopyBoolPtr(s.Enabled),
|
||||
Settings: s.Settings.Copy(),
|
||||
}
|
||||
}
|
||||
|
||||
// mergeWith merges the other settings into any
|
||||
// unset field of the receiver settings object.
|
||||
func (s *Shadowsocks) mergeWith(other Shadowsocks) {
|
||||
s.Enabled = helpers.MergeWithBool(s.Enabled, other.Enabled)
|
||||
s.Settings.MergeWith(other.Settings)
|
||||
}
|
||||
|
||||
// overrideWith overrides fields of the receiver
|
||||
// settings object with any field set in the other
|
||||
// settings.
|
||||
func (s *Shadowsocks) overrideWith(other Shadowsocks) {
|
||||
s.Enabled = helpers.OverrideWithBool(s.Enabled, other.Enabled)
|
||||
s.Settings.OverrideWith(other.Settings)
|
||||
}
|
||||
|
||||
func (s *Shadowsocks) setDefaults() {
|
||||
s.Enabled = helpers.DefaultBool(s.Enabled, false)
|
||||
s.Settings.SetDefaults()
|
||||
}
|
||||
|
||||
func (s Shadowsocks) String() string {
|
||||
return s.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (s Shadowsocks) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("Shadowsocks server settings:")
|
||||
|
||||
node.Appendf("Enabled: %s", helpers.BoolPtrToYesNo(s.Enabled))
|
||||
if !*s.Enabled {
|
||||
return node
|
||||
}
|
||||
|
||||
// TODO have ToLinesNode in qdm12/ss-server
|
||||
node.Appendf("Listening address: %s", s.Address)
|
||||
node.Appendf("Cipher: %s", s.CipherName)
|
||||
node.Appendf("Password: %s", helpers.ObfuscatePassword(*s.Password))
|
||||
node.Appendf("Log addresses: %s", helpers.BoolPtrToYesNo(s.LogAddresses))
|
||||
|
||||
return node
|
||||
}
|
||||
56
internal/configuration/settings/surfshark_retro.go
Normal file
56
internal/configuration/settings/surfshark_retro.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func surfsharkRetroRegion(selection ServerSelection) (
|
||||
updatedSelection ServerSelection) {
|
||||
locationData := constants.SurfsharkLocationData()
|
||||
|
||||
retroToLocation := make(map[string]models.SurfsharkLocationData, len(locationData))
|
||||
for _, data := range locationData {
|
||||
if data.RetroLoc == "" {
|
||||
continue
|
||||
}
|
||||
retroToLocation[strings.ToLower(data.RetroLoc)] = data
|
||||
}
|
||||
|
||||
for i, region := range selection.Regions {
|
||||
location, ok := retroToLocation[region]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
selection.Regions[i] = strings.ToLower(location.Region)
|
||||
selection.Countries = append(selection.Countries, strings.ToLower(location.Country))
|
||||
selection.Cities = append(selection.Cities, strings.ToLower(location.City)) // even empty string
|
||||
selection.Hostnames = append(selection.Hostnames, location.Hostname)
|
||||
}
|
||||
|
||||
selection.Regions = dedupSlice(selection.Regions)
|
||||
selection.Countries = dedupSlice(selection.Countries)
|
||||
selection.Cities = dedupSlice(selection.Cities)
|
||||
selection.Hostnames = dedupSlice(selection.Hostnames)
|
||||
|
||||
return selection
|
||||
}
|
||||
|
||||
func dedupSlice(slice []string) (deduped []string) {
|
||||
if slice == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
deduped = make([]string, 0, len(slice))
|
||||
seen := make(map[string]struct{}, len(slice))
|
||||
for _, s := range slice {
|
||||
if _, ok := seen[s]; !ok {
|
||||
seen[s] = struct{}{}
|
||||
deduped = append(deduped, s)
|
||||
}
|
||||
}
|
||||
|
||||
return deduped
|
||||
}
|
||||
61
internal/configuration/settings/system.go
Normal file
61
internal/configuration/settings/system.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
// System contains settings to configure system related elements.
|
||||
type System struct {
|
||||
PUID *uint16
|
||||
PGID *uint16
|
||||
Timezone string
|
||||
}
|
||||
|
||||
// Validate validates System settings.
|
||||
func (s System) validate() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *System) copy() (copied System) {
|
||||
return System{
|
||||
PUID: helpers.CopyUint16Ptr(s.PUID),
|
||||
PGID: helpers.CopyUint16Ptr(s.PGID),
|
||||
Timezone: s.Timezone,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *System) mergeWith(other System) {
|
||||
s.PUID = helpers.MergeWithUint16(s.PUID, other.PUID)
|
||||
s.PGID = helpers.MergeWithUint16(s.PGID, other.PGID)
|
||||
s.Timezone = helpers.MergeWithString(s.Timezone, other.Timezone)
|
||||
}
|
||||
|
||||
func (s *System) overrideWith(other System) {
|
||||
s.PUID = helpers.OverrideWithUint16(s.PUID, other.PUID)
|
||||
s.PGID = helpers.OverrideWithUint16(s.PGID, other.PGID)
|
||||
s.Timezone = helpers.OverrideWithString(s.Timezone, other.Timezone)
|
||||
}
|
||||
|
||||
func (s *System) setDefaults() {
|
||||
const defaultID = 1000
|
||||
s.PUID = helpers.DefaultUint16(s.PUID, defaultID)
|
||||
s.PGID = helpers.DefaultUint16(s.PGID, defaultID)
|
||||
}
|
||||
|
||||
func (s System) String() string {
|
||||
return s.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (s System) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("OS Alpine settings:")
|
||||
|
||||
node.Appendf("Process UID: %d", *s.PUID)
|
||||
node.Appendf("Process GID: %d", *s.PGID)
|
||||
|
||||
if s.Timezone != "" {
|
||||
node.Appendf("Timezone: %s", s.Timezone)
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
193
internal/configuration/settings/unbound.go
Normal file
193
internal/configuration/settings/unbound.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/qdm12/dns/pkg/provider"
|
||||
"github.com/qdm12/dns/pkg/unbound"
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gotree"
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
// Unbound is settings for the Unbound program.
|
||||
type Unbound struct {
|
||||
Providers []string
|
||||
Caching *bool
|
||||
IPv6 *bool
|
||||
VerbosityLevel *uint8
|
||||
VerbosityDetailsLevel *uint8
|
||||
ValidationLogLevel *uint8
|
||||
Username string
|
||||
Allowed []netaddr.IPPrefix
|
||||
}
|
||||
|
||||
func (u *Unbound) setDefaults() {
|
||||
if len(u.Providers) == 0 {
|
||||
u.Providers = []string{
|
||||
provider.Cloudflare().String(),
|
||||
}
|
||||
}
|
||||
|
||||
u.Caching = helpers.DefaultBool(u.Caching, true)
|
||||
u.IPv6 = helpers.DefaultBool(u.IPv6, false)
|
||||
|
||||
const defaultVerbosityLevel = 1
|
||||
u.VerbosityLevel = helpers.DefaultUint8(u.VerbosityLevel, defaultVerbosityLevel)
|
||||
|
||||
const defaultVerbosityDetailsLevel = 0
|
||||
u.VerbosityDetailsLevel = helpers.DefaultUint8(u.VerbosityDetailsLevel, defaultVerbosityDetailsLevel)
|
||||
|
||||
const defaultValidationLogLevel = 0
|
||||
u.ValidationLogLevel = helpers.DefaultUint8(u.ValidationLogLevel, defaultValidationLogLevel)
|
||||
|
||||
if u.Allowed == nil {
|
||||
u.Allowed = []netaddr.IPPrefix{
|
||||
netaddr.IPPrefixFrom(netaddr.IPv4(0, 0, 0, 0), 0),
|
||||
netaddr.IPPrefixFrom(netaddr.IPv6Raw([16]byte{}), 0),
|
||||
}
|
||||
}
|
||||
|
||||
u.Username = helpers.DefaultString(u.Username, "root")
|
||||
}
|
||||
|
||||
var (
|
||||
ErrUnboundVerbosityLevelNotValid = errors.New("Unbound verbosity level is not valid")
|
||||
ErrUnboundVerbosityDetailsLevelNotValid = errors.New("Unbound verbosity details level is not valid")
|
||||
ErrUnboundValidationLogLevelNotValid = errors.New("Unbound validation log level is not valid")
|
||||
)
|
||||
|
||||
func (u Unbound) validate() (err error) {
|
||||
for _, s := range u.Providers {
|
||||
_, err := provider.Parse(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
const maxVerbosityLevel = 5
|
||||
if *u.VerbosityLevel > maxVerbosityLevel {
|
||||
return fmt.Errorf("%w: %d must be between 0 and %d",
|
||||
ErrUnboundVerbosityLevelNotValid,
|
||||
*u.VerbosityLevel,
|
||||
maxVerbosityLevel)
|
||||
}
|
||||
|
||||
const maxVerbosityDetailsLevel = 4
|
||||
if *u.VerbosityDetailsLevel > maxVerbosityDetailsLevel {
|
||||
return fmt.Errorf("%w: %d must be between 0 and %d",
|
||||
ErrUnboundVerbosityDetailsLevelNotValid,
|
||||
*u.VerbosityDetailsLevel,
|
||||
maxVerbosityDetailsLevel)
|
||||
}
|
||||
|
||||
const maxValidationLogLevel = 2
|
||||
if *u.ValidationLogLevel > maxValidationLogLevel {
|
||||
return fmt.Errorf("%w: %d must be between 0 and %d",
|
||||
ErrUnboundValidationLogLevelNotValid,
|
||||
*u.ValidationLogLevel, maxValidationLogLevel)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u Unbound) copy() (copied Unbound) {
|
||||
return Unbound{
|
||||
Providers: helpers.CopyStringSlice(u.Providers),
|
||||
Caching: helpers.CopyBoolPtr(u.Caching),
|
||||
IPv6: helpers.CopyBoolPtr(u.IPv6),
|
||||
VerbosityLevel: helpers.CopyUint8Ptr(u.VerbosityLevel),
|
||||
VerbosityDetailsLevel: helpers.CopyUint8Ptr(u.VerbosityDetailsLevel),
|
||||
ValidationLogLevel: helpers.CopyUint8Ptr(u.ValidationLogLevel),
|
||||
Username: u.Username,
|
||||
Allowed: helpers.CopyIPPrefixSlice(u.Allowed),
|
||||
}
|
||||
}
|
||||
|
||||
func (u *Unbound) mergeWith(other Unbound) {
|
||||
u.Providers = helpers.MergeStringSlices(u.Providers, other.Providers)
|
||||
u.Caching = helpers.MergeWithBool(u.Caching, other.Caching)
|
||||
u.IPv6 = helpers.MergeWithBool(u.IPv6, other.IPv6)
|
||||
u.VerbosityLevel = helpers.MergeWithUint8(u.VerbosityLevel, other.VerbosityLevel)
|
||||
u.VerbosityDetailsLevel = helpers.MergeWithUint8(u.VerbosityDetailsLevel, other.VerbosityDetailsLevel)
|
||||
u.ValidationLogLevel = helpers.MergeWithUint8(u.ValidationLogLevel, other.ValidationLogLevel)
|
||||
u.Username = helpers.MergeWithString(u.Username, other.Username)
|
||||
u.Allowed = helpers.MergeIPPrefixesSlices(u.Allowed, other.Allowed)
|
||||
}
|
||||
|
||||
func (u *Unbound) overrideWith(other Unbound) {
|
||||
u.Providers = helpers.OverrideWithStringSlice(u.Providers, other.Providers)
|
||||
u.Caching = helpers.OverrideWithBool(u.Caching, other.Caching)
|
||||
u.IPv6 = helpers.OverrideWithBool(u.IPv6, other.IPv6)
|
||||
u.VerbosityLevel = helpers.OverrideWithUint8(u.VerbosityLevel, other.VerbosityLevel)
|
||||
u.VerbosityDetailsLevel = helpers.OverrideWithUint8(u.VerbosityDetailsLevel, other.VerbosityDetailsLevel)
|
||||
u.ValidationLogLevel = helpers.OverrideWithUint8(u.ValidationLogLevel, other.ValidationLogLevel)
|
||||
u.Username = helpers.OverrideWithString(u.Username, other.Username)
|
||||
u.Allowed = helpers.OverrideWithIPPrefixesSlice(u.Allowed, other.Allowed)
|
||||
}
|
||||
|
||||
func (u Unbound) ToUnboundFormat() (settings unbound.Settings, err error) {
|
||||
providers := make([]provider.Provider, len(u.Providers))
|
||||
for i := range providers {
|
||||
providers[i], err = provider.Parse(u.Providers[i])
|
||||
if err != nil {
|
||||
return settings, err
|
||||
}
|
||||
}
|
||||
|
||||
const port = 53
|
||||
|
||||
return unbound.Settings{
|
||||
ListeningPort: port,
|
||||
IPv4: true,
|
||||
Providers: providers,
|
||||
Caching: *u.Caching,
|
||||
IPv6: *u.IPv6,
|
||||
VerbosityLevel: *u.VerbosityLevel,
|
||||
VerbosityDetailsLevel: *u.VerbosityDetailsLevel,
|
||||
ValidationLogLevel: *u.ValidationLogLevel,
|
||||
AccessControl: unbound.AccessControlSettings{
|
||||
Allowed: u.Allowed,
|
||||
},
|
||||
Username: u.Username,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (u Unbound) GetFirstPlaintextIPv4() (ipv4 net.IP, err error) {
|
||||
s := u.Providers[0]
|
||||
provider, err := provider.Parse(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return provider.DNS().IPv4[0], nil
|
||||
}
|
||||
|
||||
func (u Unbound) String() string {
|
||||
return u.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (u Unbound) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("Unbound settings:")
|
||||
|
||||
authServers := node.Appendf("Authoritative servers:")
|
||||
for _, provider := range u.Providers {
|
||||
authServers.Appendf(provider)
|
||||
}
|
||||
|
||||
node.Appendf("Caching: %s", helpers.BoolPtrToYesNo(u.Caching))
|
||||
node.Appendf("IPv6: %s", helpers.BoolPtrToYesNo(u.IPv6))
|
||||
node.Appendf("Verbosity level: %d", *u.VerbosityLevel)
|
||||
node.Appendf("Verbosity details level: %d", *u.VerbosityDetailsLevel)
|
||||
node.Appendf("Validation log level: %d", *u.ValidationLogLevel)
|
||||
node.Appendf("System user: %s", u.Username)
|
||||
|
||||
allowedNetworks := node.Appendf("Allowed networks:")
|
||||
for _, network := range u.Allowed {
|
||||
allowedNetworks.Appendf(network.String())
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
43
internal/configuration/settings/unbound_test.go
Normal file
43
internal/configuration/settings/unbound_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
func Test_Unbound_JSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
settings := Unbound{
|
||||
Providers: []string{"cloudflare"},
|
||||
Caching: boolPtr(true),
|
||||
IPv6: boolPtr(false),
|
||||
VerbosityLevel: uint8Ptr(1),
|
||||
VerbosityDetailsLevel: nil,
|
||||
ValidationLogLevel: uint8Ptr(0),
|
||||
Username: "user",
|
||||
Allowed: []netaddr.IPPrefix{
|
||||
netaddr.IPPrefixFrom(netaddr.IPv4(0, 0, 0, 0), 0),
|
||||
netaddr.IPPrefixFrom(netaddr.IPv6Raw([16]byte{}), 0),
|
||||
},
|
||||
}
|
||||
|
||||
b, err := json.Marshal(settings)
|
||||
require.NoError(t, err)
|
||||
|
||||
const expected = `{"Providers":["cloudflare"],"Caching":true,"IPv6":false,` +
|
||||
`"VerbosityLevel":1,"VerbosityDetailsLevel":null,"ValidationLogLevel":0,` +
|
||||
`"Username":"user","Allowed":["0.0.0.0/0","::/0"]}`
|
||||
|
||||
assert.Equal(t, expected, string(b))
|
||||
|
||||
var resultSettings Unbound
|
||||
err = json.Unmarshal(b, &resultSettings)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, settings, resultSettings)
|
||||
}
|
||||
113
internal/configuration/settings/updater.go
Normal file
113
internal/configuration/settings/updater.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
// Updater contains settings to configure the VPN
|
||||
// server information updater.
|
||||
type Updater struct {
|
||||
// Period is the period for which the updater
|
||||
// should run. It can be set to 0 to disable the
|
||||
// updater. It cannot be nil in the internal state.
|
||||
// TODO change to value and add Enabled field.
|
||||
Period *time.Duration
|
||||
// DNSAddress is the DNS server address to use
|
||||
// to resolve VPN server hostnames to IP addresses.
|
||||
// It cannot be nil in the internal state.
|
||||
DNSAddress net.IP
|
||||
// Providers is the list of VPN service providers
|
||||
// to update server information for.
|
||||
Providers []string
|
||||
// CLI is to precise the updater is running in CLI
|
||||
// mode. This is set automatically and cannot be set
|
||||
// by settings sources. It cannot be nil in the
|
||||
// internal state.
|
||||
CLI *bool
|
||||
}
|
||||
|
||||
func (u Updater) Validate() (err error) {
|
||||
const minPeriod = time.Minute
|
||||
if *u.Period > 0 && *u.Period < minPeriod {
|
||||
return fmt.Errorf("%w: %d must be larger than %s",
|
||||
ErrUpdaterPeriodTooSmall, *u.Period, minPeriod)
|
||||
}
|
||||
|
||||
for i, provider := range u.Providers {
|
||||
valid := false
|
||||
for _, validProvider := range constants.AllProviders() {
|
||||
if provider == validProvider {
|
||||
valid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
return fmt.Errorf("%w: %s at index %d",
|
||||
ErrVPNProviderNameNotValid, provider, i)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Updater) copy() (copied Updater) {
|
||||
return Updater{
|
||||
Period: helpers.CopyDurationPtr(u.Period),
|
||||
DNSAddress: helpers.CopyIP(u.DNSAddress),
|
||||
Providers: helpers.CopyStringSlice(u.Providers),
|
||||
CLI: u.CLI,
|
||||
}
|
||||
}
|
||||
|
||||
// mergeWith merges the other settings into any
|
||||
// unset field of the receiver settings object.
|
||||
func (u *Updater) mergeWith(other Updater) {
|
||||
u.Period = helpers.MergeWithDuration(u.Period, other.Period)
|
||||
u.DNSAddress = helpers.MergeWithIP(u.DNSAddress, other.DNSAddress)
|
||||
u.Providers = helpers.MergeStringSlices(u.Providers, other.Providers)
|
||||
u.CLI = helpers.MergeWithBool(u.CLI, other.CLI)
|
||||
}
|
||||
|
||||
// overrideWith overrides fields of the receiver
|
||||
// settings object with any field set in the other
|
||||
// settings.
|
||||
func (u *Updater) overrideWith(other Updater) {
|
||||
u.Period = helpers.OverrideWithDuration(u.Period, other.Period)
|
||||
u.DNSAddress = helpers.OverrideWithIP(u.DNSAddress, other.DNSAddress)
|
||||
u.Providers = helpers.OverrideWithStringSlice(u.Providers, other.Providers)
|
||||
u.CLI = helpers.MergeWithBool(u.CLI, other.CLI)
|
||||
}
|
||||
|
||||
func (u *Updater) setDefaults() {
|
||||
u.Period = helpers.DefaultDuration(u.Period, 0)
|
||||
u.DNSAddress = helpers.DefaultIP(u.DNSAddress, net.IPv4(1, 1, 1, 1))
|
||||
u.CLI = helpers.DefaultBool(u.CLI, false)
|
||||
}
|
||||
|
||||
func (u Updater) String() string {
|
||||
return u.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (u Updater) toLinesNode() (node *gotree.Node) {
|
||||
if *u.Period == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
node = gotree.New("Server data updater settings:")
|
||||
node.Appendf("Update period: %s", *u.Period)
|
||||
node.Appendf("DNS address: %s", u.DNSAddress)
|
||||
node.Appendf("Providers to update: %s", strings.Join(u.Providers, ", "))
|
||||
|
||||
if *u.CLI {
|
||||
node.Appendf("CLI mode: enabled")
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
53
internal/configuration/settings/version.go
Normal file
53
internal/configuration/settings/version.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
// Version contains settings to configure the version
|
||||
// information fetcher.
|
||||
type Version struct {
|
||||
// Enabled is true if the version information should
|
||||
// be fetched from Github.
|
||||
Enabled *bool
|
||||
}
|
||||
|
||||
func (v Version) validate() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Version) copy() (copied Version) {
|
||||
return Version{
|
||||
Enabled: helpers.CopyBoolPtr(v.Enabled),
|
||||
}
|
||||
}
|
||||
|
||||
// mergeWith merges the other settings into any
|
||||
// unset field of the receiver settings object.
|
||||
func (v *Version) mergeWith(other Version) {
|
||||
v.Enabled = helpers.MergeWithBool(v.Enabled, other.Enabled)
|
||||
}
|
||||
|
||||
// overrideWith overrides fields of the receiver
|
||||
// settings object with any field set in the other
|
||||
// settings.
|
||||
func (v *Version) overrideWith(other Version) {
|
||||
v.Enabled = helpers.OverrideWithBool(v.Enabled, other.Enabled)
|
||||
}
|
||||
|
||||
func (v *Version) setDefaults() {
|
||||
v.Enabled = helpers.DefaultBool(v.Enabled, true)
|
||||
}
|
||||
|
||||
func (v Version) String() string {
|
||||
return v.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (v Version) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("Version settings:")
|
||||
|
||||
node.Appendf("Enabled: %s", helpers.BoolPtrToYesNo(v.Enabled))
|
||||
|
||||
return node
|
||||
}
|
||||
98
internal/configuration/settings/vpn.go
Normal file
98
internal/configuration/settings/vpn.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gotree"
|
||||
)
|
||||
|
||||
type VPN struct {
|
||||
// Type is the VPN type and can only be
|
||||
// 'openvpn' or 'wireguard'. It cannot be the
|
||||
// empty string in the internal state.
|
||||
Type string
|
||||
Provider Provider
|
||||
OpenVPN OpenVPN
|
||||
Wireguard Wireguard
|
||||
}
|
||||
|
||||
// Validate validates VPN settings.
|
||||
func (v VPN) validate(allServers models.AllServers) (err error) {
|
||||
// Validate Type
|
||||
validVPNTypes := []string{constants.OpenVPN, constants.Wireguard}
|
||||
if !helpers.IsOneOf(v.Type, validVPNTypes...) {
|
||||
return fmt.Errorf("%w: %q and can only be one of %s",
|
||||
ErrVPNTypeNotValid, v.Type, strings.Join(validVPNTypes, ", "))
|
||||
}
|
||||
|
||||
err = v.Provider.validate(v.Type, allServers)
|
||||
if err != nil {
|
||||
return fmt.Errorf("provider settings validation failed: %w", err)
|
||||
}
|
||||
|
||||
if v.Type == constants.OpenVPN {
|
||||
err := v.OpenVPN.validate(*v.Provider.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("OpenVPN settings validation failed: %w", err)
|
||||
}
|
||||
} else {
|
||||
err := v.Wireguard.validate(*v.Provider.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Wireguard settings validation failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *VPN) copy() (copied VPN) {
|
||||
return VPN{
|
||||
Type: v.Type,
|
||||
Provider: v.Provider.copy(),
|
||||
OpenVPN: v.OpenVPN.copy(),
|
||||
Wireguard: v.Wireguard.copy(),
|
||||
}
|
||||
}
|
||||
|
||||
func (v *VPN) mergeWith(other VPN) {
|
||||
v.Type = helpers.MergeWithString(v.Type, other.Type)
|
||||
v.Provider.mergeWith(other.Provider)
|
||||
v.OpenVPN.mergeWith(other.OpenVPN)
|
||||
v.Wireguard.mergeWith(other.Wireguard)
|
||||
}
|
||||
|
||||
func (v *VPN) overrideWith(other VPN) {
|
||||
v.Type = helpers.OverrideWithString(v.Type, other.Type)
|
||||
v.Provider.overrideWith(other.Provider)
|
||||
v.OpenVPN.overrideWith(other.OpenVPN)
|
||||
v.Wireguard.overrideWith(other.Wireguard)
|
||||
}
|
||||
|
||||
func (v *VPN) setDefaults() {
|
||||
v.Type = helpers.DefaultString(v.Type, constants.OpenVPN)
|
||||
v.Provider.setDefaults()
|
||||
v.OpenVPN.setDefaults(*v.Provider.Name)
|
||||
v.Wireguard.setDefaults()
|
||||
}
|
||||
|
||||
func (v VPN) String() string {
|
||||
return v.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (v VPN) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("VPN settings:")
|
||||
|
||||
node.AppendNode(v.Provider.toLinesNode())
|
||||
|
||||
if v.Type == constants.OpenVPN {
|
||||
node.AppendNode(v.OpenVPN.toLinesNode())
|
||||
} else {
|
||||
node.AppendNode(v.Wireguard.toLinesNode())
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
138
internal/configuration/settings/wireguard.go
Normal file
138
internal/configuration/settings/wireguard.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gotree"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
)
|
||||
|
||||
// Wireguard contains settings to configure the Wireguard client.
|
||||
type Wireguard struct {
|
||||
// PrivateKey is the Wireguard client peer private key.
|
||||
// It cannot be nil in the internal state.
|
||||
PrivateKey *string
|
||||
// PreSharedKey is the Wireguard pre-shared key.
|
||||
// It can be the empty string to indicate there
|
||||
// is no pre-shared key.
|
||||
// It cannot be nil in the internal state.
|
||||
PreSharedKey *string
|
||||
// Addresses are the Wireguard interface addresses.
|
||||
Addresses []net.IPNet
|
||||
// Interface is the name of the Wireguard interface
|
||||
// to create. It cannot be the empty string in the
|
||||
// internal state.
|
||||
Interface string
|
||||
}
|
||||
|
||||
var regexpInterfaceName = regexp.MustCompile(`^[a-zA-Z0-9_]+$`)
|
||||
|
||||
// Validate validates Wireguard settings.
|
||||
// It should only be ran if the VPN type chosen is Wireguard.
|
||||
func (w Wireguard) validate(vpnProvider string) (err error) {
|
||||
if !helpers.IsOneOf(vpnProvider,
|
||||
constants.Custom,
|
||||
constants.Ivpn,
|
||||
constants.Mullvad,
|
||||
constants.Windscribe,
|
||||
) {
|
||||
// do not validate for VPN provider not supporting Wireguard
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate PrivateKey
|
||||
if *w.PrivateKey == "" {
|
||||
return ErrWireguardPrivateKeyNotSet
|
||||
}
|
||||
_, err = wgtypes.ParseKey(*w.PrivateKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %s", ErrWireguardPrivateKeyNotValid, err)
|
||||
}
|
||||
|
||||
// Validate PreSharedKey
|
||||
if *w.PreSharedKey != "" { // Note: this is optional
|
||||
_, err = wgtypes.ParseKey(*w.PreSharedKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %s", ErrWireguardPreSharedKeyNotValid, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate Addresses
|
||||
if len(w.Addresses) == 0 {
|
||||
return ErrWireguardInterfaceAddressNotSet
|
||||
}
|
||||
for i, ipNet := range w.Addresses {
|
||||
if ipNet.IP == nil || ipNet.Mask == nil {
|
||||
return fmt.Errorf("%w: for address at index %d: %s",
|
||||
ErrWireguardInterfaceAddressNotSet, i, ipNet.String())
|
||||
}
|
||||
}
|
||||
|
||||
// Validate interface
|
||||
if !regexpInterfaceName.MatchString(w.Interface) {
|
||||
return fmt.Errorf("%w: '%s' does not match regex '%s'",
|
||||
ErrWireguardInterfaceNotValid, w.Interface, regexpInterfaceName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Wireguard) copy() (copied Wireguard) {
|
||||
return Wireguard{
|
||||
PrivateKey: helpers.CopyStringPtr(w.PrivateKey),
|
||||
PreSharedKey: helpers.CopyStringPtr(w.PreSharedKey),
|
||||
Addresses: helpers.CopyIPNetSlice(w.Addresses),
|
||||
Interface: w.Interface,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Wireguard) mergeWith(other Wireguard) {
|
||||
w.PrivateKey = helpers.MergeWithStringPtr(w.PrivateKey, other.PrivateKey)
|
||||
w.PreSharedKey = helpers.MergeWithStringPtr(w.PreSharedKey, other.PreSharedKey)
|
||||
w.Addresses = helpers.MergeIPNetsSlices(w.Addresses, other.Addresses)
|
||||
w.Interface = helpers.MergeWithString(w.Interface, other.Interface)
|
||||
}
|
||||
|
||||
func (w *Wireguard) overrideWith(other Wireguard) {
|
||||
w.PrivateKey = helpers.OverrideWithStringPtr(w.PrivateKey, other.PrivateKey)
|
||||
w.PreSharedKey = helpers.OverrideWithStringPtr(w.PreSharedKey, other.PreSharedKey)
|
||||
w.Addresses = helpers.OverrideWithIPNetsSlice(w.Addresses, other.Addresses)
|
||||
w.Interface = helpers.OverrideWithString(w.Interface, other.Interface)
|
||||
}
|
||||
|
||||
func (w *Wireguard) setDefaults() {
|
||||
w.PrivateKey = helpers.DefaultStringPtr(w.PrivateKey, "")
|
||||
w.PreSharedKey = helpers.DefaultStringPtr(w.PreSharedKey, "")
|
||||
w.Interface = helpers.DefaultString(w.Interface, "wg0")
|
||||
}
|
||||
|
||||
func (w Wireguard) String() string {
|
||||
return w.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (w Wireguard) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("Wireguard settings:")
|
||||
|
||||
if *w.PrivateKey != "" {
|
||||
s := helpers.ObfuscateWireguardKey(*w.PrivateKey)
|
||||
node.Appendf("Private key: %s", s)
|
||||
}
|
||||
|
||||
if *w.PreSharedKey != "" {
|
||||
s := helpers.ObfuscateWireguardKey(*w.PreSharedKey)
|
||||
node.Appendf("Pre-shared key: %s", s)
|
||||
}
|
||||
|
||||
addressesNode := node.Appendf("Interface addresses:")
|
||||
for _, address := range w.Addresses {
|
||||
addressesNode.Appendf(address.String())
|
||||
}
|
||||
|
||||
node.Appendf("Network interface: %s", w.Interface)
|
||||
|
||||
return node
|
||||
}
|
||||
144
internal/configuration/settings/wireguardselection.go
Normal file
144
internal/configuration/settings/wireguardselection.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gotree"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
)
|
||||
|
||||
type WireguardSelection struct {
|
||||
// EndpointIP is the server endpoint IP address.
|
||||
// It is only used with VPN providers generating Wireguard
|
||||
// configurations specific to each server and user.
|
||||
// To indicate it should not be used, it should be set
|
||||
// to the empty net.IP{} slice. It can never be nil
|
||||
// in the internal state.
|
||||
EndpointIP net.IP
|
||||
// EndpointPort is a the server port to use for the VPN server.
|
||||
// It is optional for VPN providers IVPN, Mullvad
|
||||
// and Windscribe, and compulsory for the others.
|
||||
// When optional, it can be set to 0 to indicate not use
|
||||
// a custom endpoint port. It cannot be nil in the internal
|
||||
// state.
|
||||
EndpointPort *uint16
|
||||
// PublicKey is the server public key.
|
||||
// It is only used with VPN providers generating Wireguard
|
||||
// configurations specific to each server and user.
|
||||
PublicKey string
|
||||
}
|
||||
|
||||
// Validate validates WireguardSelection settings.
|
||||
// It should only be ran if the VPN type chosen is Wireguard.
|
||||
func (w WireguardSelection) validate(vpnProvider string) (err error) {
|
||||
// Validate EndpointIP
|
||||
switch vpnProvider {
|
||||
case constants.Ivpn, constants.Mullvad, constants.Windscribe: // endpoint IP addresses are baked in
|
||||
case constants.Custom:
|
||||
if len(w.EndpointIP) == 0 {
|
||||
return ErrWireguardEndpointIPNotSet
|
||||
}
|
||||
default: // Providers not supporting Wireguard
|
||||
}
|
||||
|
||||
// Validate EndpointPort
|
||||
switch vpnProvider {
|
||||
// EndpointPort is required
|
||||
case constants.Custom:
|
||||
if *w.EndpointPort == 0 {
|
||||
return ErrWireguardEndpointPortNotSet
|
||||
}
|
||||
case constants.Ivpn, constants.Mullvad, constants.Windscribe:
|
||||
// EndpointPort is optional and can be 0
|
||||
if *w.EndpointPort == 0 {
|
||||
break // no custom endpoint port set
|
||||
}
|
||||
if vpnProvider == constants.Mullvad {
|
||||
break // no restriction on custom endpoint port value
|
||||
}
|
||||
var allowed []uint16
|
||||
switch vpnProvider {
|
||||
case constants.Ivpn:
|
||||
allowed = []uint16{2049, 2050, 53, 30587, 41893, 48574, 58237}
|
||||
case constants.Windscribe:
|
||||
allowed = []uint16{53, 80, 123, 443, 1194, 65142}
|
||||
}
|
||||
|
||||
if helpers.Uint16IsOneOf(*w.EndpointPort, allowed) {
|
||||
break
|
||||
}
|
||||
return fmt.Errorf("%w: %d for VPN service provider %s; %s",
|
||||
ErrWireguardEndpointPortNotAllowed, w.EndpointPort, vpnProvider,
|
||||
helpers.PortChoicesOrString(allowed))
|
||||
default: // Providers not supporting Wireguard
|
||||
}
|
||||
|
||||
// Validate PublicKey
|
||||
switch vpnProvider {
|
||||
case constants.Ivpn, constants.Mullvad, constants.Windscribe: // public keys are baked in
|
||||
case constants.Custom:
|
||||
if w.PublicKey == "" {
|
||||
return ErrWireguardPublicKeyNotSet
|
||||
}
|
||||
default: // Providers not supporting Wireguard
|
||||
}
|
||||
if w.PublicKey != "" {
|
||||
_, err := wgtypes.ParseKey(w.PublicKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %s: %s",
|
||||
ErrWireguardPublicKeyNotValid, w.PublicKey, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *WireguardSelection) copy() (copied WireguardSelection) {
|
||||
return WireguardSelection{
|
||||
EndpointIP: helpers.CopyIP(w.EndpointIP),
|
||||
EndpointPort: helpers.CopyUint16Ptr(w.EndpointPort),
|
||||
PublicKey: w.PublicKey,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WireguardSelection) mergeWith(other WireguardSelection) {
|
||||
w.EndpointIP = helpers.MergeWithIP(w.EndpointIP, other.EndpointIP)
|
||||
w.EndpointPort = helpers.MergeWithUint16(w.EndpointPort, other.EndpointPort)
|
||||
w.PublicKey = helpers.MergeWithString(w.PublicKey, other.PublicKey)
|
||||
}
|
||||
|
||||
func (w *WireguardSelection) overrideWith(other WireguardSelection) {
|
||||
w.EndpointIP = helpers.OverrideWithIP(w.EndpointIP, other.EndpointIP)
|
||||
w.EndpointPort = helpers.OverrideWithUint16(w.EndpointPort, other.EndpointPort)
|
||||
w.PublicKey = helpers.OverrideWithString(w.PublicKey, other.PublicKey)
|
||||
}
|
||||
|
||||
func (w *WireguardSelection) setDefaults() {
|
||||
w.EndpointIP = helpers.DefaultIP(w.EndpointIP, net.IP{})
|
||||
w.EndpointPort = helpers.DefaultUint16(w.EndpointPort, 0)
|
||||
}
|
||||
|
||||
func (w WireguardSelection) String() string {
|
||||
return w.toLinesNode().String()
|
||||
}
|
||||
|
||||
func (w WireguardSelection) toLinesNode() (node *gotree.Node) {
|
||||
node = gotree.New("Wireguard selection settings:")
|
||||
|
||||
if len(w.EndpointIP) > 0 {
|
||||
node.Appendf("Endpoint IP address: %s", w.EndpointIP)
|
||||
}
|
||||
|
||||
if *w.EndpointPort != 0 {
|
||||
node.Appendf("Endpoint port: %d", *w.EndpointPort)
|
||||
}
|
||||
|
||||
if w.PublicKey != "" {
|
||||
node.Appendf("Server public key: %s", w.PublicKey)
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
Reference in New Issue
Block a user