chore(config): upgrade to gosettings v0.4.0

- drop qdm12/govalid dependency
- upgrade qdm12/ss-server to v0.6.0
- do not unset sensitive config settings (makes no sense to me)
This commit is contained in:
Quentin McGaw
2024-03-25 19:14:20 +00:00
parent 23b0320cfb
commit ecc80a5a9e
88 changed files with 1371 additions and 2621 deletions

View File

@@ -1,55 +0,0 @@
package env
import (
"fmt"
"net/netip"
"github.com/qdm12/gluetun/internal/configuration/settings"
)
func (s *Source) readDNS() (dns settings.DNS, err error) {
dns.ServerAddress, err = s.readDNSServerAddress()
if err != nil {
return dns, err
}
dns.KeepNameserver, err = s.env.BoolPtr("DNS_KEEP_NAMESERVER")
if err != nil {
return dns, err
}
dns.DoT, err = s.readDoT()
if err != nil {
return dns, fmt.Errorf("DoT settings: %w", err)
}
return dns, nil
}
func (s *Source) readDNSServerAddress() (address netip.Addr, err error) {
const currentKey = "DNS_ADDRESS"
key := firstKeySet(s.env, "DNS_PLAINTEXT_ADDRESS", currentKey)
switch key {
case "":
return address, nil
case currentKey:
default: // Retro-compatibility
s.handleDeprecatedKey(key, currentKey)
}
address, err = s.env.NetipAddr(key)
if err != nil {
return address, err
}
// TODO remove in v4
if address.Unmap().Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) != 0 {
s.warner.Warn(key + " is set to " + address.String() +
" so the DNS over TLS (DoT) server will not be used." +
" The default value changed to 127.0.0.1 so it uses the internal DoT serves." +
" If the DoT server fails to start, the IPv4 address of the first plaintext DNS server" +
" corresponding to the first DoT provider chosen is used.")
}
return address, nil
}

View File

@@ -1,73 +0,0 @@
package env
import (
"errors"
"fmt"
"net/netip"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gosettings/sources/env"
)
func (s *Source) readDNSBlacklist() (blacklist settings.DNSBlacklist, err error) {
blacklist.BlockMalicious, err = s.env.BoolPtr("BLOCK_MALICIOUS")
if err != nil {
return blacklist, err
}
blacklist.BlockSurveillance, err = s.env.BoolPtr("BLOCK_SURVEILLANCE",
env.RetroKeys("BLOCK_NSA"))
if err != nil {
return blacklist, err
}
blacklist.BlockAds, err = s.env.BoolPtr("BLOCK_ADS")
if err != nil {
return blacklist, err
}
blacklist.AddBlockedIPs, blacklist.AddBlockedIPPrefixes,
err = s.readDoTPrivateAddresses() // TODO v4 split in 2
if err != nil {
return blacklist, err
}
blacklist.AllowedHosts = s.env.CSV("UNBLOCK") // TODO v4 change name
return blacklist, nil
}
var (
ErrPrivateAddressNotValid = errors.New("private address is not a valid IP or CIDR range")
)
func (s *Source) readDoTPrivateAddresses() (ips []netip.Addr,
ipPrefixes []netip.Prefix, err error) {
privateAddresses := s.env.CSV("DOT_PRIVATE_ADDRESS")
if len(privateAddresses) == 0 {
return nil, nil, nil
}
ips = make([]netip.Addr, 0, len(privateAddresses))
ipPrefixes = make([]netip.Prefix, 0, len(privateAddresses))
for _, privateAddress := range privateAddresses {
ip, err := netip.ParseAddr(privateAddress)
if err == nil {
ips = append(ips, ip)
continue
}
ipPrefix, err := netip.ParsePrefix(privateAddress)
if err == nil {
ipPrefixes = append(ipPrefixes, ipPrefix)
continue
}
return nil, nil, fmt.Errorf(
"environment variable DOT_PRIVATE_ADDRESS: %w: %s",
ErrPrivateAddressNotValid, privateAddress)
}
return ips, ipPrefixes, nil
}

View File

@@ -1,29 +0,0 @@
package env
import (
"github.com/qdm12/gluetun/internal/configuration/settings"
)
func (s *Source) readDoT() (dot settings.DoT, err error) {
dot.Enabled, err = s.env.BoolPtr("DOT")
if err != nil {
return dot, err
}
dot.UpdatePeriod, err = s.env.DurationPtr("DNS_UPDATE_PERIOD")
if err != nil {
return dot, err
}
dot.Unbound, err = s.readUnbound()
if err != nil {
return dot, err
}
dot.Blacklist, err = s.readDNSBlacklist()
if err != nil {
return dot, err
}
return dot, nil
}

View File

@@ -1,36 +0,0 @@
package env
import (
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gosettings/sources/env"
)
func (s *Source) readFirewall() (firewall settings.Firewall, err error) {
firewall.VPNInputPorts, err = s.env.CSVUint16("FIREWALL_VPN_INPUT_PORTS")
if err != nil {
return firewall, err
}
firewall.InputPorts, err = s.env.CSVUint16("FIREWALL_INPUT_PORTS")
if err != nil {
return firewall, err
}
firewall.OutboundSubnets, err = s.env.CSVNetipPrefixes("FIREWALL_OUTBOUND_SUBNETS",
env.RetroKeys("EXTRA_SUBNETS"))
if err != nil {
return firewall, err
}
firewall.Enabled, err = s.env.BoolPtr("FIREWALL")
if err != nil {
return firewall, err
}
firewall.Debug, err = s.env.BoolPtr("FIREWALL_DEBUG")
if err != nil {
return firewall, err
}
return firewall, nil
}

View File

@@ -1,35 +0,0 @@
package env
import (
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gosettings/sources/env"
)
func (s *Source) ReadHealth() (health settings.Health, err error) {
health.ServerAddress = s.env.String("HEALTH_SERVER_ADDRESS")
health.TargetAddress = s.env.String("HEALTH_TARGET_ADDRESS",
env.RetroKeys("HEALTH_ADDRESS_TO_PING"))
successWaitPtr, err := s.env.DurationPtr("HEALTH_SUCCESS_WAIT_DURATION")
if err != nil {
return health, err
} else if successWaitPtr != nil {
health.SuccessWait = *successWaitPtr
}
health.VPN.Initial, err = s.env.DurationPtr(
"HEALTH_VPN_DURATION_INITIAL",
env.RetroKeys("HEALTH_OPENVPN_DURATION_INITIAL"))
if err != nil {
return health, err
}
health.VPN.Addition, err = s.env.DurationPtr(
"HEALTH_VPN_DURATION_ADDITION",
env.RetroKeys("HEALTH_OPENVPN_DURATION_ADDITION"))
if err != nil {
return health, err
}
return health, nil
}

View File

@@ -1,33 +0,0 @@
package env
import (
"fmt"
"os"
"github.com/qdm12/gosettings/sources/env"
)
func unsetEnvKeys(envKeys []string, err error) (newErr error) {
newErr = err
for _, envKey := range envKeys {
unsetErr := os.Unsetenv(envKey)
if unsetErr != nil && newErr == nil {
newErr = fmt.Errorf("unsetting environment variable %s: %w", envKey, unsetErr)
}
}
return newErr
}
func ptrTo[T any](value T) *T {
return &value
}
func firstKeySet(e env.Env, keys ...string) (firstKeySet string) {
for _, key := range keys {
value := e.Get(key)
if value != nil {
return key
}
}
return ""
}

View File

@@ -1,84 +0,0 @@
package env
import (
"fmt"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gosettings/sources/env"
"github.com/qdm12/govalid/binary"
)
func (s *Source) readHTTPProxy() (httpProxy settings.HTTPProxy, err error) {
httpProxy.User = s.env.Get("HTTPPROXY_USER",
env.RetroKeys("PROXY_USER", "TINYPROXY_USER"),
env.ForceLowercase(false))
httpProxy.Password = s.env.Get("HTTPPROXY_PASSWORD",
env.RetroKeys("PROXY_PASSWORD", "TINYPROXY_PASSWORD"),
env.ForceLowercase(false))
httpProxy.ListeningAddress, err = s.readHTTProxyListeningAddress()
if err != nil {
return httpProxy, err
}
httpProxy.Enabled, err = s.env.BoolPtr("HTTPPROXY", env.RetroKeys("PROXY", "TINYPROXY"))
if err != nil {
return httpProxy, err
}
httpProxy.Stealth, err = s.env.BoolPtr("HTTPPROXY_STEALTH")
if err != nil {
return httpProxy, err
}
httpProxy.Log, err = s.readHTTProxyLog()
if err != nil {
return httpProxy, err
}
return httpProxy, nil
}
func (s *Source) readHTTProxyListeningAddress() (listeningAddress string, err error) {
const currentKey = "HTTPPROXY_LISTENING_ADDRESS"
key := firstKeySet(s.env, "HTTPPROXY_PORT", "TINYPROXY_PORT", "PROXY_PORT",
currentKey)
switch key {
case "":
return "", nil
case currentKey:
return s.env.String(key), nil
}
// Retro-compatible keys using a port only
s.handleDeprecatedKey(key, currentKey)
port, err := s.env.Uint16Ptr(key)
if err != nil {
return "", err
}
return fmt.Sprintf(":%d", *port), nil
}
func (s *Source) readHTTProxyLog() (enabled *bool, err error) {
const currentKey = "HTTPPROXY_LOG"
key := firstKeySet(s.env, "PROXY_LOG", "TINYPROXY_LOG", "HTTPPROXY_LOG")
switch key {
case "":
return nil, nil //nolint:nilnil
case currentKey:
return s.env.BoolPtr(key)
}
// Retro-compatible keys using different boolean verbs
s.handleDeprecatedKey(key, currentKey)
value := s.env.String(key)
retroOption := binary.OptionEnabled("on", "info", "connect", "notice")
enabled, err = binary.Validate(value, retroOption)
if err != nil {
return nil, fmt.Errorf("environment variable %s: %w", key, err)
}
return enabled, nil
}

View File

@@ -1,53 +0,0 @@
package env
import (
"errors"
"fmt"
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/log"
)
func (s *Source) readLog() (log settings.Log, err error) {
log.Level, err = s.readLogLevel()
if err != nil {
return log, err
}
return log, nil
}
func (s *Source) readLogLevel() (level *log.Level, err error) {
value := s.env.String("LOG_LEVEL")
if value == "" {
return nil, nil //nolint:nilnil
}
level = new(log.Level)
*level, err = parseLogLevel(value)
if err != nil {
return nil, fmt.Errorf("environment variable LOG_LEVEL: %w", err)
}
return level, nil
}
var ErrLogLevelUnknown = errors.New("log level is unknown")
func parseLogLevel(s string) (level log.Level, err error) {
switch strings.ToLower(s) {
case "debug":
return log.LevelDebug, nil
case "info":
return log.LevelInfo, nil
case "warning":
return log.LevelWarn, nil
case "error":
return log.LevelError, nil
default:
return level, fmt.Errorf(
"%w: %q is not valid and can be one of debug, info, warning or error",
ErrLogLevelUnknown, s)
}
}

View File

@@ -1,77 +0,0 @@
package env
import (
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gosettings/sources/env"
)
func (s *Source) readOpenVPN() (
openVPN settings.OpenVPN, err error) {
defer func() {
err = unsetEnvKeys([]string{"OPENVPN_KEY", "OPENVPN_CERT",
"OPENVPN_KEY_PASSPHRASE", "OPENVPN_ENCRYPTED_KEY"}, err)
}()
openVPN.Version = s.env.String("OPENVPN_VERSION")
openVPN.User = s.env.Get("OPENVPN_USER",
env.RetroKeys("USER"), env.ForceLowercase(false))
openVPN.Password = s.env.Get("OPENVPN_PASSWORD",
env.RetroKeys("PASSWORD"), env.ForceLowercase(false))
openVPN.ConfFile = s.env.Get("OPENVPN_CUSTOM_CONFIG", env.ForceLowercase(false))
openVPN.Ciphers = s.env.CSV("OPENVPN_CIPHERS", env.RetroKeys("OPENVPN_CIPHER"))
openVPN.Auth = s.env.Get("OPENVPN_AUTH")
openVPN.Cert = s.env.Get("OPENVPN_CERT", env.ForceLowercase(false))
openVPN.Key = s.env.Get("OPENVPN_KEY", env.ForceLowercase(false))
openVPN.EncryptedKey = s.env.Get("OPENVPN_ENCRYPTED_KEY", env.ForceLowercase(false))
openVPN.KeyPassphrase = s.env.Get("OPENVPN_KEY_PASSPHRASE", env.ForceLowercase(false))
openVPN.PIAEncPreset = s.readPIAEncryptionPreset()
openVPN.MSSFix, err = s.env.Uint16Ptr("OPENVPN_MSSFIX")
if err != nil {
return openVPN, err
}
openVPN.Interface = s.env.String("VPN_INTERFACE",
env.RetroKeys("OPENVPN_INTERFACE"), env.ForceLowercase(false))
openVPN.ProcessUser, err = s.readOpenVPNProcessUser()
if err != nil {
return openVPN, err
}
openVPN.Verbosity, err = s.env.IntPtr("OPENVPN_VERBOSITY")
if err != nil {
return openVPN, err
}
flagsPtr := s.env.Get("OPENVPN_FLAGS", env.ForceLowercase(false))
if flagsPtr != nil {
openVPN.Flags = strings.Fields(*flagsPtr)
}
return openVPN, nil
}
func (s *Source) readPIAEncryptionPreset() (presetPtr *string) {
return s.env.Get(
"PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET",
env.RetroKeys("ENCRYPTION", "PIA_ENCRYPTION"))
}
func (s *Source) readOpenVPNProcessUser() (processUser string, err error) {
value, err := s.env.BoolPtr("OPENVPN_ROOT") // Retro-compatibility
if err != nil {
return "", err
} else if value != nil {
if *value {
return "root", nil
}
const defaultNonRootUser = "nonrootuser"
return defaultNonRootUser, nil
}
return s.env.String("OPENVPN_PROCESS_USER"), nil
}

View File

@@ -1,26 +0,0 @@
package env
import (
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gosettings/sources/env"
)
func (s *Source) readOpenVPNSelection() (
selection settings.OpenVPNSelection, err error) {
selection.ConfFile = s.env.Get("OPENVPN_CUSTOM_CONFIG", env.ForceLowercase(false))
selection.Protocol = s.env.String("OPENVPN_PROTOCOL", env.RetroKeys("PROTOCOL"))
if err != nil {
return selection, err
}
selection.CustomPort, err = s.env.Uint16Ptr("VPN_ENDPOINT_PORT",
env.RetroKeys("PORT", "OPENVPN_PORT"))
if err != nil {
return selection, err
}
selection.PIAEncPreset = s.readPIAEncryptionPreset()
return selection, nil
}

View File

@@ -1,34 +0,0 @@
package env
import (
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gosettings/sources/env"
)
func (s *Source) readPortForward() (
portForwarding settings.PortForwarding, err error) {
portForwarding.Enabled, err = s.env.BoolPtr("VPN_PORT_FORWARDING",
env.RetroKeys(
"PORT_FORWARDING",
"PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING",
))
if err != nil {
return portForwarding, err
}
portForwarding.Provider = s.env.Get("VPN_PORT_FORWARDING_PROVIDER")
portForwarding.Filepath = s.env.Get("VPN_PORT_FORWARDING_STATUS_FILE",
env.ForceLowercase(false),
env.RetroKeys(
"PORT_FORWARDING_STATUS_FILE",
"PRIVATE_INTERNET_ACCESS_VPN_PORT_FORWARDING_STATUS_FILE",
))
portForwarding.ListeningPort, err = s.env.Uint16Ptr("VPN_PORT_FORWARDING_LISTENING_PORT")
if err != nil {
return portForwarding, err
}
return portForwarding, nil
}

View File

@@ -1,26 +0,0 @@
package env
import (
"github.com/qdm12/gluetun/internal/pprof"
)
func (s *Source) readPprof() (settings pprof.Settings, err error) {
settings.Enabled, err = s.env.BoolPtr("PPROF_ENABLED")
if err != nil {
return settings, err
}
settings.BlockProfileRate, err = s.env.IntPtr("PPROF_BLOCK_PROFILE_RATE")
if err != nil {
return settings, err
}
settings.MutexProfileRate, err = s.env.IntPtr("PPROF_MUTEX_PROFILE_RATE")
if err != nil {
return settings, err
}
settings.HTTPServer.Address = s.env.String("PPROF_HTTP_SERVER_ADDRESS")
return settings, nil
}

View File

@@ -1,45 +0,0 @@
package env
import (
"fmt"
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gosettings/sources/env"
)
func (s *Source) readProvider(vpnType string) (provider settings.Provider, err error) {
provider.Name = s.readVPNServiceProvider(vpnType)
provider.ServerSelection, err = s.readServerSelection(provider.Name, vpnType)
if err != nil {
return provider, fmt.Errorf("server selection: %w", err)
}
provider.PortForwarding, err = s.readPortForward()
if err != nil {
return provider, fmt.Errorf("port forwarding: %w", err)
}
return provider, nil
}
func (s *Source) readVPNServiceProvider(vpnType string) (vpnProvider string) {
vpnProvider = s.env.String("VPN_SERVICE_PROVIDER", env.RetroKeys("VPNSP"))
if vpnProvider == "" {
if vpnType != vpn.Wireguard && s.env.Get("OPENVPN_CUSTOM_CONFIG") != nil {
// retro compatibility
return providers.Custom
}
return ""
}
vpnProvider = strings.ToLower(vpnProvider)
if vpnProvider == "pia" { // retro compatibility
return providers.PrivateInternetAccess
}
return vpnProvider
}

View File

@@ -1,22 +0,0 @@
package env
import (
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gosettings/sources/env"
)
func (s *Source) readPublicIP() (publicIP settings.PublicIP, err error) {
publicIP.Period, err = s.env.DurationPtr("PUBLICIP_PERIOD")
if err != nil {
return publicIP, err
}
publicIP.IPFilepath = s.env.Get("PUBLICIP_FILE",
env.ForceLowercase(false), env.RetroKeys("IP_STATUS_FILE"))
publicIP.API = s.env.String("PUBLICIP_API")
publicIP.APIToken = s.env.Get("PUBLICIP_API_TOKEN")
return publicIP, nil
}

View File

@@ -1,103 +0,0 @@
package env
import (
"os"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gosettings/sources/env"
)
type Source struct {
env env.Env
warner Warner
handleDeprecatedKey func(deprecatedKey, newKey string)
}
type Warner interface {
Warn(s string)
}
func New(warner Warner) *Source {
handleDeprecatedKey := func(deprecatedKey, newKey string) {
warner.Warn(
"You are using the old environment variable " + deprecatedKey +
", please consider changing it to " + newKey)
}
return &Source{
env: *env.New(os.Environ(), handleDeprecatedKey),
warner: warner,
handleDeprecatedKey: handleDeprecatedKey,
}
}
func (s *Source) String() string { return "environment variables" }
func (s *Source) Read() (settings settings.Settings, err error) {
settings.VPN, err = s.readVPN()
if err != nil {
return settings, err
}
settings.Firewall, err = s.readFirewall()
if err != nil {
return settings, err
}
settings.System, err = s.readSystem()
if err != nil {
return settings, err
}
settings.Health, err = s.ReadHealth()
if err != nil {
return settings, err
}
settings.HTTPProxy, err = s.readHTTPProxy()
if err != nil {
return settings, err
}
settings.Log, err = s.readLog()
if err != nil {
return settings, err
}
settings.PublicIP, err = s.readPublicIP()
if err != nil {
return settings, err
}
settings.Updater, err = s.readUpdater()
if err != nil {
return settings, err
}
settings.Version, err = s.readVersion()
if err != nil {
return settings, err
}
settings.Shadowsocks, err = s.readShadowsocks()
if err != nil {
return settings, err
}
settings.DNS, err = s.readDNS()
if err != nil {
return settings, err
}
settings.ControlServer, err = s.readControlServer()
if err != nil {
return settings, err
}
settings.Pprof, err = s.readPprof()
if err != nil {
return settings, err
}
return settings, nil
}

View File

@@ -1,16 +0,0 @@
package env
import (
"github.com/qdm12/gluetun/internal/configuration/settings"
)
func (s *Source) readControlServer() (controlServer settings.ControlServer, err error) {
controlServer.Log, err = s.env.BoolPtr("HTTP_CONTROL_SERVER_LOG")
if err != nil {
return controlServer, err
}
controlServer.Address = s.env.Get("HTTP_CONTROL_SERVER_ADDRESS")
return controlServer, nil
}

View File

@@ -1,92 +0,0 @@
package env
import (
"errors"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gosettings/sources/env"
)
func (s *Source) readServerSelection(vpnProvider, vpnType string) (
ss settings.ServerSelection, err error) {
ss.VPN = vpnType
ss.TargetIP, err = s.env.NetipAddr("VPN_ENDPOINT_IP",
env.RetroKeys("OPENVPN_TARGET_IP"))
if err != nil {
return ss, err
}
ss.Countries = s.env.CSV("SERVER_COUNTRIES", env.RetroKeys("COUNTRY"))
if vpnProvider == providers.Cyberghost && len(ss.Countries) == 0 {
// Retro-compatibility for Cyberghost using the REGION variable
ss.Countries = s.env.CSV("REGION")
if len(ss.Countries) > 0 {
s.handleDeprecatedKey("REGION", "SERVER_COUNTRIES")
}
}
ss.Regions = s.env.CSV("SERVER_REGIONS", env.RetroKeys("REGION"))
ss.Cities = s.env.CSV("SERVER_CITIES", env.RetroKeys("CITY"))
ss.ISPs = s.env.CSV("ISP")
ss.Hostnames = s.env.CSV("SERVER_HOSTNAMES", env.RetroKeys("SERVER_HOSTNAME"))
ss.Names = s.env.CSV("SERVER_NAMES", env.RetroKeys("SERVER_NAME"))
ss.Numbers, err = s.env.CSVUint16("SERVER_NUMBER")
ss.Categories = s.env.CSV("SERVER_CATEGORIES")
if err != nil {
return ss, err
}
// Mullvad only
ss.OwnedOnly, err = s.env.BoolPtr("OWNED_ONLY", env.RetroKeys("OWNED"))
if err != nil {
return ss, err
}
// VPNUnlimited and ProtonVPN only
ss.FreeOnly, err = s.env.BoolPtr("FREE_ONLY")
if err != nil {
return ss, err
}
// VPNSecure only
ss.PremiumOnly, err = s.env.BoolPtr("PREMIUM_ONLY")
if err != nil {
return ss, err
}
// Surfshark only
ss.MultiHopOnly, err = s.env.BoolPtr("MULTIHOP_ONLY")
if err != nil {
return ss, err
}
// VPNUnlimited only
ss.StreamOnly, err = s.env.BoolPtr("STREAM_ONLY")
if err != nil {
return ss, err
}
// PIA only
ss.PortForwardOnly, err = s.env.BoolPtr("PORT_FORWARD_ONLY")
if err != nil {
return ss, err
}
ss.OpenVPN, err = s.readOpenVPNSelection()
if err != nil {
return ss, err
}
ss.Wireguard, err = s.readWireguardSelection()
if err != nil {
return ss, err
}
return ss, nil
}
var (
ErrInvalidIP = errors.New("invalid IP address")
)

View File

@@ -1,42 +0,0 @@
package env
import (
"fmt"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gosettings/sources/env"
)
func (s *Source) readShadowsocks() (shadowsocks settings.Shadowsocks, err error) {
shadowsocks.Enabled, err = s.env.BoolPtr("SHADOWSOCKS")
if err != nil {
return shadowsocks, err
}
shadowsocks.Address, err = s.readShadowsocksAddress()
if err != nil {
return shadowsocks, err
}
shadowsocks.LogAddresses, err = s.env.BoolPtr("SHADOWSOCKS_LOG")
if err != nil {
return shadowsocks, err
}
shadowsocks.CipherName = s.env.String("SHADOWSOCKS_CIPHER",
env.RetroKeys("SHADOWSOCKS_METHOD"))
shadowsocks.Password = s.env.Get("SHADOWSOCKS_PASSWORD", env.ForceLowercase(false))
return shadowsocks, nil
}
func (s *Source) readShadowsocksAddress() (address *string, err error) {
const currentKey = "SHADOWSOCKS_LISTENING_ADDRESS"
port, err := s.env.Uint16Ptr("SHADOWSOCKS_PORT") // retro-compatibility
if err != nil {
return nil, err
} else if port != nil {
s.handleDeprecatedKey("SHADOWSOCKS_PORT", currentKey)
return ptrTo(fmt.Sprintf(":%d", *port)), nil
}
return s.env.Get(currentKey), nil
}

View File

@@ -1,22 +0,0 @@
package env
import (
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gosettings/sources/env"
)
func (s *Source) readSystem() (system settings.System, err error) {
system.PUID, err = s.env.Uint32Ptr("PUID", env.RetroKeys("UID"))
if err != nil {
return system, err
}
system.PGID, err = s.env.Uint32Ptr("PGID", env.RetroKeys("GID"))
if err != nil {
return system, err
}
system.Timezone = s.env.String("TZ")
return system, nil
}

View File

@@ -1,36 +0,0 @@
package env
import (
"github.com/qdm12/gluetun/internal/configuration/settings"
)
func (s *Source) readUnbound() (unbound settings.Unbound, err error) {
unbound.Providers = s.env.CSV("DOT_PROVIDERS")
unbound.Caching, err = s.env.BoolPtr("DOT_CACHING")
if err != nil {
return unbound, err
}
unbound.IPv6, err = s.env.BoolPtr("DOT_IPV6")
if err != nil {
return unbound, err
}
unbound.VerbosityLevel, err = s.env.Uint8Ptr("DOT_VERBOSITY")
if err != nil {
return unbound, err
}
unbound.VerbosityDetailsLevel, err = s.env.Uint8Ptr("DOT_VERBOSITY_DETAILS")
if err != nil {
return unbound, err
}
unbound.ValidationLogLevel, err = s.env.Uint8Ptr("DOT_VALIDATION_LOGLEVEL")
if err != nil {
return unbound, err
}
return unbound, nil
}

View File

@@ -1,35 +0,0 @@
package env
import (
"github.com/qdm12/gluetun/internal/configuration/settings"
)
func (s *Source) readUpdater() (updater settings.Updater, err error) {
updater.Period, err = s.env.DurationPtr("UPDATER_PERIOD")
if err != nil {
return updater, err
}
updater.DNSAddress, err = readUpdaterDNSAddress()
if err != nil {
return updater, err
}
updater.MinRatio, err = s.env.Float64("UPDATER_MIN_RATIO")
if err != nil {
return updater, err
}
updater.Providers = s.env.CSV("UPDATER_VPN_SERVICE_PROVIDERS")
return updater, nil
}
func readUpdaterDNSAddress() (address string, err error) {
// TODO this is currently using Cloudflare in
// plaintext to not be blocked by DNS over TLS by default.
// If a plaintext address is set in the DNS settings, this one will be used.
// use custom future encrypted DNS written in Go without blocking
// as it's too much trouble to start another parallel unbound instance for now.
return "", nil
}

View File

@@ -1,14 +0,0 @@
package env
import (
"github.com/qdm12/gluetun/internal/configuration/settings"
)
func (s *Source) readVersion() (version settings.Version, err error) {
version.Enabled, err = s.env.BoolPtr("VERSION_INFORMATION")
if err != nil {
return version, err
}
return version, nil
}

View File

@@ -1,28 +0,0 @@
package env
import (
"fmt"
"github.com/qdm12/gluetun/internal/configuration/settings"
)
func (s *Source) readVPN() (vpn settings.VPN, err error) {
vpn.Type = s.env.String("VPN_TYPE")
vpn.Provider, err = s.readProvider(vpn.Type)
if err != nil {
return vpn, fmt.Errorf("VPN provider: %w", err)
}
vpn.OpenVPN, err = s.readOpenVPN()
if err != nil {
return vpn, fmt.Errorf("OpenVPN: %w", err)
}
vpn.Wireguard, err = s.readWireguard()
if err != nil {
return vpn, fmt.Errorf("wireguard: %w", err)
}
return vpn, nil
}

View File

@@ -1,33 +0,0 @@
package env
import (
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gosettings/sources/env"
)
func (s *Source) readWireguard() (wireguard settings.Wireguard, err error) {
defer func() {
err = unsetEnvKeys([]string{"WIREGUARD_PRIVATE_KEY", "WIREGUARD_PRESHARED_KEY"}, err)
}()
wireguard.PrivateKey = s.env.Get("WIREGUARD_PRIVATE_KEY", env.ForceLowercase(false))
wireguard.PreSharedKey = s.env.Get("WIREGUARD_PRESHARED_KEY", env.ForceLowercase(false))
wireguard.Interface = s.env.String("VPN_INTERFACE",
env.RetroKeys("WIREGUARD_INTERFACE"), env.ForceLowercase(false))
wireguard.Implementation = s.env.String("WIREGUARD_IMPLEMENTATION")
wireguard.Addresses, err = s.env.CSVNetipPrefixes("WIREGUARD_ADDRESSES",
env.RetroKeys("WIREGUARD_ADDRESS"))
if err != nil {
return wireguard, err // already wrapped
}
wireguard.AllowedIPs, err = s.env.CSVNetipPrefixes("WIREGUARD_ALLOWED_IPS")
if err != nil {
return wireguard, err // already wrapped
}
mtuPtr, err := s.env.Uint16Ptr("WIREGUARD_MTU")
if err != nil {
return wireguard, err
} else if mtuPtr != nil {
wireguard.MTU = *mtuPtr
}
return wireguard, nil
}

View File

@@ -1,23 +0,0 @@
package env
import (
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gosettings/sources/env"
)
func (s *Source) readWireguardSelection() (
selection settings.WireguardSelection, err error) {
selection.EndpointIP, err = s.env.NetipAddr("VPN_ENDPOINT_IP", env.RetroKeys("WIREGUARD_ENDPOINT_IP"))
if err != nil {
return selection, err
}
selection.EndpointPort, err = s.env.Uint16Ptr("VPN_ENDPOINT_PORT", env.RetroKeys("WIREGUARD_ENDPOINT_PORT"))
if err != nil {
return selection, err
}
selection.PublicKey = s.env.String("WIREGUARD_PUBLIC_KEY", env.ForceLowercase(false))
return selection, nil
}

View File

@@ -1,5 +0,0 @@
package files
import "github.com/qdm12/gluetun/internal/configuration/settings"
func (s *Source) ReadHealth() (settings settings.Health, err error) { return settings, nil }

View File

@@ -9,47 +9,45 @@ import (
"github.com/qdm12/gluetun/internal/openvpn/extract"
)
// ReadFromFile reads the content of the file as a string.
// It returns a nil string pointer if the file does not exist.
func ReadFromFile(filepath string) (s *string, err error) {
// ReadFromFile reads the content of the file as a string,
// and returns if the file was present or not with isSet.
func ReadFromFile(filepath string) (content string, isSet bool, err error) {
file, err := os.Open(filepath)
if err != nil {
if os.IsNotExist(err) {
return nil, nil //nolint:nilnil
return "", false, nil
}
return nil, err
return "", false, fmt.Errorf("opening file: %w", err)
}
b, err := io.ReadAll(file)
if err != nil {
_ = file.Close()
return nil, err
return "", false, fmt.Errorf("reading file: %w", err)
}
if err := file.Close(); err != nil {
return nil, err
return "", false, fmt.Errorf("closing file: %w", err)
}
content := string(b)
content = string(b)
content = strings.TrimSuffix(content, "\r\n")
content = strings.TrimSuffix(content, "\n")
return &content, nil
return content, true, nil
}
func readPEMFile(filepath string) (base64Ptr *string, err error) {
pemData, err := ReadFromFile(filepath)
func ReadPEMFile(filepath string) (base64Str string, isSet bool, err error) {
pemData, isSet, err := ReadFromFile(filepath)
if err != nil {
return nil, fmt.Errorf("reading file: %w", err)
return "", false, fmt.Errorf("reading file: %w", err)
} else if !isSet {
return "", false, nil
}
if pemData == nil {
return nil, nil //nolint:nilnil
}
base64Data, err := extract.PEM([]byte(*pemData))
base64Str, err = extract.PEM([]byte(pemData))
if err != nil {
return nil, fmt.Errorf("extracting base64 encoded data from PEM content: %w", err)
return "", false, fmt.Errorf("extracting base64 encoded data from PEM content: %w", err)
}
return &base64Data, nil
return base64Str, true, nil
}

View File

@@ -1,3 +0,0 @@
package files
func ptrTo[T any](x T) *T { return &x }

View File

@@ -0,0 +1,5 @@
package files
type Warner interface {
Warnf(format string, a ...interface{})
}

View File

@@ -1,33 +0,0 @@
package files
import (
"fmt"
"github.com/qdm12/gluetun/internal/configuration/settings"
)
const (
// OpenVPNClientKeyPath is the OpenVPN client key filepath.
OpenVPNClientKeyPath = "/gluetun/client.key"
// OpenVPNClientCertificatePath is the OpenVPN client certificate filepath.
OpenVPNClientCertificatePath = "/gluetun/client.crt"
openVPNEncryptedKey = "/gluetun/openvpn_encrypted_key"
)
func (s *Source) readOpenVPN() (settings settings.OpenVPN, err error) {
settings.Key, err = readPEMFile(OpenVPNClientKeyPath)
if err != nil {
return settings, fmt.Errorf("client key: %w", err)
}
settings.Cert, err = readPEMFile(OpenVPNClientCertificatePath)
if err != nil {
return settings, fmt.Errorf("client certificate: %w", err)
}
settings.EncryptedKey, err = readPEMFile(openVPNEncryptedKey)
if err != nil {
return settings, fmt.Errorf("reading encrypted key file: %w", err)
}
return settings, nil
}

View File

@@ -1,16 +0,0 @@
package files
import (
"fmt"
"github.com/qdm12/gluetun/internal/configuration/settings"
)
func (s *Source) readProvider() (provider settings.Provider, err error) {
provider.ServerSelection, err = s.readServerSelection()
if err != nil {
return provider, fmt.Errorf("server selection: %w", err)
}
return provider, nil
}

View File

@@ -1,32 +1,99 @@
package files
import (
"github.com/qdm12/gluetun/internal/configuration/settings"
"os"
"path/filepath"
"strings"
)
type Source struct {
wireguardConfigPath string
rootDirectory string
environ map[string]string
warner Warner
cached struct {
wireguardLoaded bool
wireguardConf WireguardConfig
}
}
func New() *Source {
const wireguardConfigPath = "/gluetun/wireguard/wg0.conf"
func New(warner Warner) (source *Source) {
osEnviron := os.Environ()
environ := make(map[string]string, len(osEnviron))
for _, pair := range osEnviron {
const maxSplit = 2
split := strings.SplitN(pair, "=", maxSplit)
environ[split[0]] = split[1]
}
return &Source{
wireguardConfigPath: wireguardConfigPath,
rootDirectory: "/gluetun",
environ: environ,
warner: warner,
}
}
func (s *Source) String() string { return "files" }
func (s *Source) Read() (settings settings.Settings, err error) {
settings.VPN, err = s.readVPN()
if err != nil {
return settings, err
func (s *Source) Get(key string) (value string, isSet bool) {
if key == "" {
return "", false
}
// TODO v4 custom environment variable to set the files parent directory
// and not to set each file to a specific path
envKey := strings.ToUpper(key)
envKey = strings.ReplaceAll(envKey, "-", "_")
envKey += "_FILE"
path := s.environ[envKey]
if path == "" {
path = filepath.Join(s.rootDirectory, key)
}
settings.System, err = s.readSystem()
if err != nil {
return settings, err
// Special file handling
switch key {
// TODO timezone from /etc/localtime
case "client.crt", "client.key":
value, isSet, err := ReadPEMFile(path)
if err != nil {
s.warner.Warnf("skipping %s: parsing PEM: %s", path, err)
}
return value, isSet
case "wireguard_private_key":
return strPtrToStringIsSet(s.lazyLoadWireguardConf().PrivateKey)
case "wireguard_preshared_key":
return strPtrToStringIsSet(s.lazyLoadWireguardConf().PreSharedKey)
case "wireguard_addresses":
return strPtrToStringIsSet(s.lazyLoadWireguardConf().Addresses)
case "wireguard_public_key":
return strPtrToStringIsSet(s.lazyLoadWireguardConf().PublicKey)
case "vpn_endpoint_ip":
return strPtrToStringIsSet(s.lazyLoadWireguardConf().EndpointIP)
case "vpn_endpoint_port":
return strPtrToStringIsSet(s.lazyLoadWireguardConf().EndpointPort)
}
return settings, nil
value, isSet, err := ReadFromFile(path)
if err != nil {
s.warner.Warnf("skipping %s: reading file: %s", path, err)
}
return value, isSet
}
func (s *Source) KeyTransform(key string) string {
switch key {
// TODO v4 remove these irregular cases
case "OPENVPN_KEY":
return "client.key"
case "OPENVPN_CERT":
return "client.crt"
default:
key = strings.ToLower(key) // HTTPROXY_USER -> httpproxy_user
return key
}
}
func strPtrToStringIsSet(ptr *string) (s string, isSet bool) {
if ptr == nil {
return "", false
}
return *ptr, true
}

View File

@@ -1,16 +0,0 @@
package files
import (
"fmt"
"github.com/qdm12/gluetun/internal/configuration/settings"
)
func (s *Source) readServerSelection() (selection settings.ServerSelection, err error) {
selection.Wireguard, err = s.readWireguardSelection()
if err != nil {
return selection, fmt.Errorf("wireguard: %w", err)
}
return selection, nil
}

View File

@@ -1,10 +0,0 @@
package files
import (
"github.com/qdm12/gluetun/internal/configuration/settings"
)
func (s *Source) readSystem() (system settings.System, err error) {
// TODO timezone from /etc/localtime
return system, nil
}

View File

@@ -1,26 +0,0 @@
package files
import (
"fmt"
"github.com/qdm12/gluetun/internal/configuration/settings"
)
func (s *Source) readVPN() (vpn settings.VPN, err error) {
vpn.Provider, err = s.readProvider()
if err != nil {
return vpn, fmt.Errorf("provider: %w", err)
}
vpn.OpenVPN, err = s.readOpenVPN()
if err != nil {
return vpn, fmt.Errorf("OpenVPN: %w", err)
}
vpn.Wireguard, err = s.readWireguard()
if err != nil {
return vpn, fmt.Errorf("wireguard: %w", err)
}
return vpn, nil
}

View File

@@ -1,124 +1,115 @@
package files
import (
"errors"
"fmt"
"net/netip"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"gopkg.in/ini.v1"
)
func (s *Source) readWireguard() (wireguard settings.Wireguard, err error) {
fileStringPtr, err := ReadFromFile(s.wireguardConfigPath)
func (s *Source) lazyLoadWireguardConf() WireguardConfig {
if s.cached.wireguardLoaded {
return s.cached.wireguardConf
}
s.cached.wireguardLoaded = true
var err error
s.cached.wireguardConf, err = ParseWireguardConf(filepath.Join(s.rootDirectory, "wg0.conf"))
if err != nil {
return wireguard, fmt.Errorf("reading file: %w", err)
s.warner.Warnf("skipping Wireguard config: %s", err)
}
return s.cached.wireguardConf
}
if fileStringPtr == nil {
return wireguard, nil
}
rawData := []byte(*fileStringPtr)
return ParseWireguardConf(rawData)
type WireguardConfig struct {
PrivateKey *string
PreSharedKey *string
Addresses *string
PublicKey *string
EndpointIP *string
EndpointPort *string
}
var (
regexINISectionNotExist = regexp.MustCompile(`^section ".+" does not exist$`)
regexINIKeyNotExist = regexp.MustCompile(`key ".*" not exists$`)
)
func ParseWireguardConf(rawData []byte) (wireguard settings.Wireguard, err error) {
iniFile, err := ini.Load(rawData)
func ParseWireguardConf(path string) (config WireguardConfig, err error) {
iniFile, err := ini.Load(path)
if err != nil {
return wireguard, fmt.Errorf("loading ini from reader: %w", err)
if errors.Is(err, os.ErrNotExist) {
return WireguardConfig{}, nil
}
return WireguardConfig{}, fmt.Errorf("loading ini from reader: %w", err)
}
interfaceSection, err := iniFile.GetSection("Interface")
if err == nil {
err = parseWireguardInterfaceSection(interfaceSection, &wireguard)
if err != nil {
return wireguard, fmt.Errorf("parsing interface section: %w", err)
}
config.PrivateKey, config.Addresses = parseWireguardInterfaceSection(interfaceSection)
} else if !regexINISectionNotExist.MatchString(err.Error()) {
// can never happen
return wireguard, fmt.Errorf("getting interface section: %w", err)
return WireguardConfig{}, fmt.Errorf("getting interface section: %w", err)
}
peerSection, err := iniFile.GetSection("Peer")
if err == nil {
wireguard.PreSharedKey, err = parseINIWireguardKey(peerSection, "PresharedKey")
if err != nil {
return wireguard, fmt.Errorf("parsing peer section: %w", err)
}
config.PreSharedKey, config.PublicKey, config.EndpointIP,
config.EndpointPort = parseWireguardPeerSection(peerSection)
} else if !regexINISectionNotExist.MatchString(err.Error()) {
// can never happen
return wireguard, fmt.Errorf("getting peer section: %w", err)
return WireguardConfig{}, fmt.Errorf("getting peer section: %w", err)
}
return wireguard, nil
return config, nil
}
func parseWireguardInterfaceSection(interfaceSection *ini.Section,
wireguard *settings.Wireguard) (err error) {
wireguard.PrivateKey, err = parseINIWireguardKey(interfaceSection, "PrivateKey")
if err != nil {
return err // error is already wrapped correctly
}
wireguard.Addresses, err = parseINIWireguardAddress(interfaceSection)
if err != nil {
return err // error is already wrapped correctly
}
return nil
func parseWireguardInterfaceSection(interfaceSection *ini.Section) (
privateKey, addresses *string) {
privateKey = getINIKeyFromSection(interfaceSection, "PrivateKey")
addresses = getINIKeyFromSection(interfaceSection, "Address")
return privateKey, addresses
}
func parseINIWireguardKey(section *ini.Section, keyName string) (
key *string, err error) {
iniKey, err := section.GetKey(keyName)
var (
ErrEndpointHostNotIP = errors.New("endpoint host is not an IP")
)
func parseWireguardPeerSection(peerSection *ini.Section) (
preSharedKey, publicKey, endpointIP, endpointPort *string) {
preSharedKey = getINIKeyFromSection(peerSection, "PresharedKey")
publicKey = getINIKeyFromSection(peerSection, "PublicKey")
endpoint := getINIKeyFromSection(peerSection, "Endpoint")
if endpoint != nil {
parts := strings.Split(*endpoint, ":")
endpointIP = &parts[0]
const partsWithPort = 2
if len(parts) >= partsWithPort {
endpointPort = new(string)
*endpointPort = strings.Join(parts[1:], ":")
}
}
return preSharedKey, publicKey, endpointIP, endpointPort
}
var (
regexINIKeyNotExist = regexp.MustCompile(`key ".*" not exists$`)
)
func getINIKeyFromSection(section *ini.Section, key string) (value *string) {
iniKey, err := section.GetKey(key)
if err != nil {
if regexINIKeyNotExist.MatchString(err.Error()) {
return nil, nil //nolint:nilnil
return nil
}
// can never happen
return nil, fmt.Errorf("getting %s key: %w", keyName, err)
panic(fmt.Sprintf("getting key %q: %s", key, err))
}
key = new(string)
*key = iniKey.String()
_, err = wgtypes.ParseKey(*key)
if err != nil {
return nil, fmt.Errorf("parsing %s: %s: %w", keyName, *key, err)
}
return key, nil
}
func parseINIWireguardAddress(section *ini.Section) (
addresses []netip.Prefix, err error) {
addressKey, err := section.GetKey("Address")
if err != nil {
if regexINIKeyNotExist.MatchString(err.Error()) {
return nil, nil
}
// can never happen
return nil, fmt.Errorf("getting Address key: %w", err)
}
addressStrings := strings.Split(addressKey.String(), ",")
addresses = make([]netip.Prefix, len(addressStrings))
for i, addressString := range addressStrings {
addressString = strings.TrimSpace(addressString)
if !strings.ContainsRune(addressString, '/') {
addressString += "/32"
}
addresses[i], err = netip.ParsePrefix(addressString)
if err != nil {
return nil, fmt.Errorf("parsing address: %w", err)
}
}
return addresses, nil
value = new(string)
*value = iniKey.String()
return value
}

View File

@@ -1,48 +1,42 @@
package files
import (
"net/netip"
"os"
"path/filepath"
"testing"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/ini.v1"
)
func Test_Source_readWireguard(t *testing.T) {
func ptrTo[T any](value T) *T { return &value }
func Test_Source_ParseWireguardConf(t *testing.T) {
t.Parallel()
t.Run("fail reading from file", func(t *testing.T) {
t.Parallel()
dirPath := t.TempDir()
source := &Source{
wireguardConfigPath: dirPath,
}
wireguard, err := source.readWireguard()
assert.Equal(t, settings.Wireguard{}, wireguard)
wireguard, err := ParseWireguardConf(dirPath)
assert.Equal(t, WireguardConfig{}, wireguard)
assert.Error(t, err)
assert.Regexp(t, `reading file: read .+: is a directory`, err.Error())
assert.Regexp(t, `loading ini from reader: BOM: read .+: is a directory`, err.Error())
})
t.Run("no file", func(t *testing.T) {
t.Parallel()
noFile := filepath.Join(t.TempDir(), "doesnotexist")
source := &Source{
wireguardConfigPath: noFile,
}
wireguard, err := source.readWireguard()
assert.Equal(t, settings.Wireguard{}, wireguard)
wireguard, err := ParseWireguardConf(noFile)
assert.Equal(t, WireguardConfig{}, wireguard)
assert.NoError(t, err)
})
testCases := map[string]struct {
fileContent string
wireguard settings.Wireguard
wireguard WireguardConfig
errMessage string
}{
"ini load error": {
@@ -50,14 +44,14 @@ func Test_Source_readWireguard(t *testing.T) {
errMessage: "loading ini from reader: key-value delimiter not found: invalid",
},
"empty file": {},
"interface section parsing error": {
"interface_section_missing": {
fileContent: `
[Interface]
PrivateKey = x
[Peer]
PresharedKey = YJ680VN+dGrdsWNjSFqZ6vvwuiNhbq502ZL3G7Q3o3g=
`,
errMessage: "parsing interface section: parsing PrivateKey: " +
"x: wgtypes: failed to parse base64-encoded key: " +
"illegal base64 data at input byte 0",
wireguard: WireguardConfig{
PreSharedKey: ptrTo("YJ680VN+dGrdsWNjSFqZ6vvwuiNhbq502ZL3G7Q3o3g="),
},
},
"success": {
fileContent: `
@@ -69,12 +63,10 @@ DNS = 193.138.218.74
[Peer]
PresharedKey = YJ680VN+dGrdsWNjSFqZ6vvwuiNhbq502ZL3G7Q3o3g=
`,
wireguard: settings.Wireguard{
wireguard: WireguardConfig{
PrivateKey: ptrTo("QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8="),
PreSharedKey: ptrTo("YJ680VN+dGrdsWNjSFqZ6vvwuiNhbq502ZL3G7Q3o3g="),
Addresses: []netip.Prefix{
netip.PrefixFrom(netip.AddrFrom4([4]byte{10, 38, 22, 35}), 32),
},
Addresses: ptrTo("10.38.22.35/32"),
},
},
}
@@ -88,11 +80,7 @@ PresharedKey = YJ680VN+dGrdsWNjSFqZ6vvwuiNhbq502ZL3G7Q3o3g=
err := os.WriteFile(configFile, []byte(testCase.fileContent), 0600)
require.NoError(t, err)
source := &Source{
wireguardConfigPath: configFile,
}
wireguard, err := source.readWireguard()
wireguard, err := ParseWireguardConf(configFile)
assert.Equal(t, testCase.wireguard, wireguard)
if testCase.errMessage != "" {
@@ -109,34 +97,26 @@ func Test_parseWireguardInterfaceSection(t *testing.T) {
testCases := map[string]struct {
iniData string
wireguard settings.Wireguard
errMessage string
privateKey *string
addresses *string
}{
"private key error": {
iniData: `[Interface]
PrivateKey = x`,
errMessage: "parsing PrivateKey: x: " +
"wgtypes: failed to parse base64-encoded key: " +
"illegal base64 data at input byte 0",
"no_fields": {
iniData: `[Interface]`,
},
"address error": {
"only_private_key": {
iniData: `[Interface]
Address = x
PrivateKey = x
`,
errMessage: "parsing address: netip.ParsePrefix(\"x/32\"): ParseAddr(\"x\"): unable to parse IP",
privateKey: ptrTo("x"),
},
"success": {
"all_fields": {
iniData: `
[Interface]
PrivateKey = QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8=
Address = 10.38.22.35/32
`,
wireguard: settings.Wireguard{
PrivateKey: ptrTo("QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8="),
Addresses: []netip.Prefix{
netip.PrefixFrom(netip.AddrFrom4([4]byte{10, 38, 22, 35}), 32),
},
},
privateKey: ptrTo("QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8="),
addresses: ptrTo("10.38.22.35/32"),
},
}
@@ -150,109 +130,74 @@ Address = 10.38.22.35/32
iniSection, err := iniFile.GetSection("Interface")
require.NoError(t, err)
var wireguard settings.Wireguard
err = parseWireguardInterfaceSection(iniSection, &wireguard)
assert.Equal(t, testCase.wireguard, wireguard)
if testCase.errMessage != "" {
assert.EqualError(t, err, testCase.errMessage)
} else {
assert.NoError(t, err)
}
})
}
}
func Test_parseINIWireguardKey(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
fileContent string
keyName string
key *string
errMessage string
}{
"key does not exist": {
fileContent: `[Interface]`,
keyName: "PrivateKey",
},
"bad Wireguard key": {
fileContent: `[Interface]
PrivateKey = x`,
keyName: "PrivateKey",
errMessage: "parsing PrivateKey: x: " +
"wgtypes: failed to parse base64-encoded key: " +
"illegal base64 data at input byte 0",
},
"success": {
fileContent: `[Interface]
PrivateKey = QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8=`,
keyName: "PrivateKey",
key: ptrTo("QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8="),
},
}
for testName, testCase := range testCases {
testCase := testCase
t.Run(testName, func(t *testing.T) {
t.Parallel()
iniFile, err := ini.Load([]byte(testCase.fileContent))
require.NoError(t, err)
iniSection, err := iniFile.GetSection("Interface")
require.NoError(t, err)
key, err := parseINIWireguardKey(iniSection, testCase.keyName)
assert.Equal(t, testCase.key, key)
if testCase.errMessage != "" {
assert.EqualError(t, err, testCase.errMessage)
} else {
assert.NoError(t, err)
}
})
}
}
func Test_parseINIWireguardAddress(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
fileContent string
addresses []netip.Prefix
errMessage string
}{
"key does not exist": {
fileContent: `[Interface]`,
},
"bad address": {
fileContent: `[Interface]
Address = x`,
errMessage: "parsing address: netip.ParsePrefix(\"x/32\"): ParseAddr(\"x\"): unable to parse IP",
},
"success": {
fileContent: `[Interface]
Address = 1.2.3.4/32, 5.6.7.8/32`,
addresses: []netip.Prefix{
netip.PrefixFrom(netip.AddrFrom4([4]byte{1, 2, 3, 4}), 32),
netip.PrefixFrom(netip.AddrFrom4([4]byte{5, 6, 7, 8}), 32),
},
},
}
for testName, testCase := range testCases {
testCase := testCase
t.Run(testName, func(t *testing.T) {
t.Parallel()
iniFile, err := ini.Load([]byte(testCase.fileContent))
require.NoError(t, err)
iniSection, err := iniFile.GetSection("Interface")
require.NoError(t, err)
addresses, err := parseINIWireguardAddress(iniSection)
privateKey, addresses := parseWireguardInterfaceSection(iniSection)
assert.Equal(t, testCase.privateKey, privateKey)
assert.Equal(t, testCase.addresses, addresses)
})
}
}
func Test_parseWireguardPeerSection(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
iniData string
preSharedKey *string
publicKey *string
endpointIP *string
endpointPort *string
errMessage string
}{
"public key set": {
iniData: `[Peer]
PublicKey = QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8=`,
publicKey: ptrTo("QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8="),
},
"endpoint_only_host": {
iniData: `[Peer]
Endpoint = x`,
endpointIP: ptrTo("x"),
},
"endpoint_no_port": {
iniData: `[Peer]
Endpoint = x:`,
endpointIP: ptrTo("x"),
endpointPort: ptrTo(""),
},
"valid_endpoint": {
iniData: `[Peer]
Endpoint = 1.2.3.4:51820`,
endpointIP: ptrTo("1.2.3.4"),
endpointPort: ptrTo("51820"),
},
"all_set": {
iniData: `[Peer]
PublicKey = QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8=
Endpoint = 1.2.3.4:51820`,
publicKey: ptrTo("QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8="),
endpointIP: ptrTo("1.2.3.4"),
endpointPort: ptrTo("51820"),
},
}
for testName, testCase := range testCases {
testCase := testCase
t.Run(testName, func(t *testing.T) {
t.Parallel()
iniFile, err := ini.Load([]byte(testCase.iniData))
require.NoError(t, err)
iniSection, err := iniFile.GetSection("Peer")
require.NoError(t, err)
preSharedKey, publicKey, endpointIP,
endpointPort := parseWireguardPeerSection(iniSection)
assert.Equal(t, testCase.preSharedKey, preSharedKey)
assert.Equal(t, testCase.publicKey, publicKey)
assert.Equal(t, testCase.endpointIP, endpointIP)
assert.Equal(t, testCase.endpointPort, endpointPort)
if testCase.errMessage != "" {
assert.EqualError(t, err, testCase.errMessage)
} else {

View File

@@ -1,83 +0,0 @@
package files
import (
"errors"
"fmt"
"net"
"net/netip"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/govalid/port"
"gopkg.in/ini.v1"
)
var (
ErrEndpointHostNotIP = errors.New("endpoint host is not an IP")
)
func (s *Source) readWireguardSelection() (selection settings.WireguardSelection, err error) {
fileStringPtr, err := ReadFromFile(s.wireguardConfigPath)
if err != nil {
return selection, fmt.Errorf("reading file: %w", err)
}
if fileStringPtr == nil {
return selection, nil
}
rawData := []byte(*fileStringPtr)
iniFile, err := ini.Load(rawData)
if err != nil {
return selection, fmt.Errorf("loading ini from reader: %w", err)
}
peerSection, err := iniFile.GetSection("Peer")
if err == nil {
err = parseWireguardPeerSection(peerSection, &selection)
if err != nil {
return selection, fmt.Errorf("parsing peer section: %w", err)
}
} else if !regexINISectionNotExist.MatchString(err.Error()) {
// can never happen
return selection, fmt.Errorf("getting peer section: %w", err)
}
return selection, nil
}
func parseWireguardPeerSection(peerSection *ini.Section,
selection *settings.WireguardSelection) (err error) {
publicKeyPtr, err := parseINIWireguardKey(peerSection, "PublicKey")
if err != nil {
return err // error is already wrapped correctly
} else if publicKeyPtr != nil {
selection.PublicKey = *publicKeyPtr
}
endpointKey, err := peerSection.GetKey("Endpoint")
if err == nil {
endpoint := endpointKey.String()
host, portString, err := net.SplitHostPort(endpoint)
if err != nil {
return fmt.Errorf("splitting endpoint: %w", err)
}
ip, err := netip.ParseAddr(host)
if err != nil {
return fmt.Errorf("%w: %w", ErrEndpointHostNotIP, err)
}
endpointPort, err := port.Validate(portString)
if err != nil {
return fmt.Errorf("port from Endpoint key: %w", err)
}
selection.EndpointIP = ip
selection.EndpointPort = &endpointPort
} else if !regexINIKeyNotExist.MatchString(err.Error()) {
// can never happen
return fmt.Errorf("getting endpoint key: %w", err)
}
return nil
}

View File

@@ -1,181 +0,0 @@
package files
import (
"net/netip"
"os"
"path/filepath"
"testing"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/ini.v1"
)
func uint16Ptr(n uint16) *uint16 { return &n }
func Test_Source_readWireguardSelection(t *testing.T) {
t.Parallel()
t.Run("fail reading from file", func(t *testing.T) {
t.Parallel()
dirPath := t.TempDir()
source := &Source{
wireguardConfigPath: dirPath,
}
wireguard, err := source.readWireguardSelection()
assert.Equal(t, settings.WireguardSelection{}, wireguard)
assert.Error(t, err)
assert.Regexp(t, `reading file: read .+: is a directory`, err.Error())
})
t.Run("no file", func(t *testing.T) {
t.Parallel()
noFile := filepath.Join(t.TempDir(), "doesnotexist")
source := &Source{
wireguardConfigPath: noFile,
}
wireguard, err := source.readWireguardSelection()
assert.Equal(t, settings.WireguardSelection{}, wireguard)
assert.NoError(t, err)
})
testCases := map[string]struct {
fileContent string
selection settings.WireguardSelection
errMessage string
}{
"ini load error": {
fileContent: "invalid",
errMessage: "loading ini from reader: key-value delimiter not found: invalid",
},
"empty file": {},
"peer section parsing error": {
fileContent: `
[Peer]
PublicKey = x
`,
errMessage: "parsing peer section: parsing PublicKey: " +
"x: wgtypes: failed to parse base64-encoded key: " +
"illegal base64 data at input byte 0",
},
"success": {
fileContent: `
[Peer]
PublicKey = QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8=
Endpoint = 1.2.3.4:51820
`,
selection: settings.WireguardSelection{
PublicKey: "QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8=",
EndpointIP: netip.AddrFrom4([4]byte{1, 2, 3, 4}),
EndpointPort: uint16Ptr(51820),
},
},
}
for testName, testCase := range testCases {
testCase := testCase
t.Run(testName, func(t *testing.T) {
t.Parallel()
configFile := filepath.Join(t.TempDir(), "wg.conf")
err := os.WriteFile(configFile, []byte(testCase.fileContent), 0600)
require.NoError(t, err)
source := &Source{
wireguardConfigPath: configFile,
}
wireguard, err := source.readWireguardSelection()
assert.Equal(t, testCase.selection, wireguard)
if testCase.errMessage != "" {
assert.EqualError(t, err, testCase.errMessage)
} else {
assert.NoError(t, err)
}
})
}
}
func Test_parseWireguardPeerSection(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
iniData string
selection settings.WireguardSelection
errMessage string
}{
"public key error": {
iniData: `[Peer]
PublicKey = x`,
errMessage: "parsing PublicKey: x: " +
"wgtypes: failed to parse base64-encoded key: " +
"illegal base64 data at input byte 0",
},
"public key set": {
iniData: `[Peer]
PublicKey = QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8=`,
selection: settings.WireguardSelection{
PublicKey: "QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8=",
},
},
"missing port in endpoint": {
iniData: `[Peer]
Endpoint = x`,
errMessage: "splitting endpoint: address x: missing port in address",
},
"endpoint host is not IP": {
iniData: `[Peer]
Endpoint = website.com:51820`,
errMessage: "endpoint host is not an IP: ParseAddr(\"website.com\"): unexpected character (at \"website.com\")",
},
"endpoint port is not valid": {
iniData: `[Peer]
Endpoint = 1.2.3.4:518299`,
errMessage: "port from Endpoint key: port cannot be higher than 65535: 518299",
},
"valid endpoint": {
iniData: `[Peer]
Endpoint = 1.2.3.4:51820`,
selection: settings.WireguardSelection{
EndpointIP: netip.AddrFrom4([4]byte{1, 2, 3, 4}),
EndpointPort: uint16Ptr(51820),
},
},
"all set": {
iniData: `[Peer]
PublicKey = QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8=
Endpoint = 1.2.3.4:51820`,
selection: settings.WireguardSelection{
PublicKey: "QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8=",
EndpointIP: netip.AddrFrom4([4]byte{1, 2, 3, 4}),
EndpointPort: uint16Ptr(51820),
},
},
}
for testName, testCase := range testCases {
testCase := testCase
t.Run(testName, func(t *testing.T) {
t.Parallel()
iniFile, err := ini.Load([]byte(testCase.iniData))
require.NoError(t, err)
iniSection, err := iniFile.GetSection("Peer")
require.NoError(t, err)
var selection settings.WireguardSelection
err = parseWireguardPeerSection(iniSection, &selection)
assert.Equal(t, testCase.selection, selection)
if testCase.errMessage != "" {
assert.EqualError(t, err, testCase.errMessage)
} else {
assert.NoError(t, err)
}
})
}
}

View File

@@ -1,69 +0,0 @@
package merge
import (
"fmt"
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings"
)
type ConfigSource interface {
Read() (settings settings.Settings, err error)
ReadHealth() (settings settings.Health, err error)
String() string
}
type Source struct {
sources []ConfigSource
}
func New(sources ...ConfigSource) *Source {
return &Source{
sources: sources,
}
}
func (s *Source) String() string {
sources := make([]string, len(s.sources))
for i := range s.sources {
sources[i] = s.sources[i].String()
}
return strings.Join(sources, ", ")
}
// Read reads the settings for each source, merging unset fields
// with field set by the next source.
// It then set defaults to remaining unset fields.
func (s *Source) Read() (settings settings.Settings, err error) {
for _, source := range s.sources {
settingsFromSource, err := source.Read()
if err != nil {
return settings, fmt.Errorf("reading from %s: %w", source, err)
}
settings.MergeWith(settingsFromSource)
}
settings.SetDefaults()
return settings, nil
}
// ReadHealth reads the health settings for each source, merging unset fields
// with field set by the next source.
// It then set defaults to remaining unset fields, and validate
// all the fields.
func (s *Source) ReadHealth() (settings settings.Health, err error) {
for _, source := range s.sources {
settingsFromSource, err := source.ReadHealth()
if err != nil {
return settings, fmt.Errorf("reading from %s: %w", source, err)
}
settings.MergeWith(settingsFromSource)
}
settings.SetDefaults()
err = settings.Validate()
if err != nil {
return settings, err
}
return settings, nil
}

View File

@@ -1,5 +0,0 @@
package secrets
import "github.com/qdm12/gluetun/internal/configuration/settings"
func (s *Source) ReadHealth() (settings settings.Health, err error) { return settings, nil }

View File

@@ -1,58 +1,8 @@
package secrets
import (
"fmt"
"net/netip"
"strings"
"github.com/qdm12/gluetun/internal/configuration/sources/files"
"github.com/qdm12/gluetun/internal/openvpn/extract"
"github.com/qdm12/gosettings/sources/env"
)
func (s *Source) readSecretFileAsStringPtr(secretPathEnvKey, defaultSecretPath string) (
stringPtr *string, err error) {
path := s.env.String(secretPathEnvKey, env.ForceLowercase(false))
if path == "" {
path = defaultSecretPath
func strPtrToStringIsSet(ptr *string) (s string, isSet bool) {
if ptr == nil {
return "", false
}
return files.ReadFromFile(path)
}
func (s *Source) readPEMSecretFile(secretPathEnvKey, defaultSecretPath string) (
base64Ptr *string, err error) {
pemData, err := s.readSecretFileAsStringPtr(secretPathEnvKey, defaultSecretPath)
if err != nil {
return nil, fmt.Errorf("reading secret file: %w", err)
}
if pemData == nil {
return nil, nil //nolint:nilnil
}
base64Data, err := extract.PEM([]byte(*pemData))
if err != nil {
return nil, fmt.Errorf("extracting base64 encoded data from PEM content: %w", err)
}
return &base64Data, nil
}
func parseAddresses(addressesCSV string) (addresses []netip.Prefix, err error) {
if addressesCSV == "" {
return nil, nil
}
addressStrings := strings.Split(addressesCSV, ",")
addresses = make([]netip.Prefix, len(addressStrings))
for i, addressString := range addressStrings {
addressString = strings.TrimSpace(addressString)
addresses[i], err = netip.ParsePrefix(addressString)
if err != nil {
return nil, fmt.Errorf("parsing address %d of %d: %w",
i+1, len(addressStrings), err)
}
}
return addresses, nil
return *ptr, true
}

View File

@@ -1,92 +0,0 @@
package secrets
import (
"os"
"path/filepath"
"testing"
"github.com/qdm12/gosettings/sources/env"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func ptrTo[T any](value T) *T { return &value }
func Test_readSecretFileAsStringPtr(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
source func(tempDir string) Source
secretPathEnvKey string
defaultSecretFileName string
setupFile func(tempDir string) error
stringPtr *string
errWrapped error
errMessage string
}{
"no_secret_file": {
defaultSecretFileName: "default_secret_file",
secretPathEnvKey: "SECRET_FILE",
},
"empty_secret_file": {
defaultSecretFileName: "default_secret_file",
secretPathEnvKey: "SECRET_FILE",
setupFile: func(tempDir string) error {
secretFilepath := filepath.Join(tempDir, "default_secret_file")
return os.WriteFile(secretFilepath, nil, os.ModePerm)
},
stringPtr: ptrTo(""),
},
"default_secret_file": {
defaultSecretFileName: "default_secret_file",
secretPathEnvKey: "SECRET_FILE",
setupFile: func(tempDir string) error {
secretFilepath := filepath.Join(tempDir, "default_secret_file")
return os.WriteFile(secretFilepath, []byte("A"), os.ModePerm)
},
stringPtr: ptrTo("A"),
},
"env_specified_secret_file": {
source: func(tempDir string) Source {
secretFilepath := filepath.Join(tempDir, "secret_file")
environ := []string{"SECRET_FILE=" + secretFilepath}
return Source{env: *env.New(environ, nil)}
},
defaultSecretFileName: "default_secret_file",
secretPathEnvKey: "SECRET_FILE",
setupFile: func(tempDir string) error {
secretFilepath := filepath.Join(tempDir, "secret_file")
return os.WriteFile(secretFilepath, []byte("B"), os.ModePerm)
},
stringPtr: ptrTo("B"),
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
var source Source
if testCase.source != nil {
source = testCase.source(tempDir)
}
defaultSecretPath := filepath.Join(tempDir, testCase.defaultSecretFileName)
if testCase.setupFile != nil {
err := testCase.setupFile(tempDir)
require.NoError(t, err)
}
stringPtr, err := source.readSecretFileAsStringPtr(
testCase.secretPathEnvKey, defaultSecretPath)
assert.Equal(t, testCase.stringPtr, stringPtr)
assert.ErrorIs(t, err, testCase.errWrapped)
if testCase.errWrapped != nil {
assert.EqualError(t, err, testCase.errMessage)
}
})
}
}

View File

@@ -1,27 +0,0 @@
package secrets
import (
"fmt"
"github.com/qdm12/gluetun/internal/configuration/settings"
)
func (s *Source) readHTTPProxy() (settings settings.HTTPProxy, err error) {
settings.User, err = s.readSecretFileAsStringPtr(
"HTTPPROXY_USER_SECRETFILE",
"/run/secrets/httpproxy_user",
)
if err != nil {
return settings, fmt.Errorf("reading HTTP proxy user secret file: %w", err)
}
settings.Password, err = s.readSecretFileAsStringPtr(
"HTTPPROXY_PASSWORD_SECRETFILE",
"/run/secrets/httpproxy_password",
)
if err != nil {
return settings, fmt.Errorf("reading HTTP proxy password secret file: %w", err)
}
return settings, nil
}

View File

@@ -0,0 +1,5 @@
package secrets
type Warner interface {
Warnf(format string, a ...interface{})
}

View File

@@ -1,60 +0,0 @@
package secrets
import (
"fmt"
"github.com/qdm12/gluetun/internal/configuration/settings"
)
func (s *Source) readOpenVPN() (
settings settings.OpenVPN, err error) {
settings.User, err = s.readSecretFileAsStringPtr(
"OPENVPN_USER_SECRETFILE",
"/run/secrets/openvpn_user",
)
if err != nil {
return settings, fmt.Errorf("reading user file: %w", err)
}
settings.Password, err = s.readSecretFileAsStringPtr(
"OPENVPN_PASSWORD_SECRETFILE",
"/run/secrets/openvpn_password",
)
if err != nil {
return settings, fmt.Errorf("reading password file: %w", err)
}
settings.Key, err = s.readPEMSecretFile(
"OPENVPN_CLIENTKEY_SECRETFILE",
"/run/secrets/openvpn_clientkey",
)
if err != nil {
return settings, fmt.Errorf("reading client key file: %w", err)
}
settings.EncryptedKey, err = s.readPEMSecretFile(
"OPENVPN_ENCRYPTED_KEY_SECRETFILE",
"/run/secrets/openvpn_encrypted_key",
)
if err != nil {
return settings, fmt.Errorf("reading encrypted key file: %w", err)
}
settings.KeyPassphrase, err = s.readSecretFileAsStringPtr(
"OPENVPN_KEY_PASSPHRASE_SECRETFILE",
"/run/secrets/openvpn_key_passphrase",
)
if err != nil {
return settings, fmt.Errorf("reading key passphrase file: %w", err)
}
settings.Cert, err = s.readPEMSecretFile(
"OPENVPN_CLIENTCRT_SECRETFILE",
"/run/secrets/openvpn_clientcrt",
)
if err != nil {
return settings, fmt.Errorf("reading client certificate file: %w", err)
}
return settings, nil
}

View File

@@ -1,46 +1,104 @@
package secrets
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gosettings/sources/env"
"github.com/qdm12/gluetun/internal/configuration/sources/files"
)
type Source struct {
env env.Env
rootDirectory string
environ map[string]string
warner Warner
cached struct {
wireguardLoaded bool
wireguardConf files.WireguardConfig
}
}
func New() *Source {
handleDeprecatedKey := (func(deprecatedKey, newKey string))(nil)
func New(warner Warner) (source *Source) {
const rootDirectory = "/run/secrets"
osEnviron := os.Environ()
environ := make(map[string]string, len(osEnviron))
for _, pair := range osEnviron {
const maxSplit = 2
split := strings.SplitN(pair, "=", maxSplit)
environ[split[0]] = split[1]
}
return &Source{
env: *env.New(os.Environ(), handleDeprecatedKey),
rootDirectory: rootDirectory,
environ: environ,
warner: warner,
}
}
func (s *Source) String() string { return "secret files" }
func (s *Source) Read() (settings settings.Settings, err error) {
settings.VPN, err = s.readVPN()
if err != nil {
return settings, err
func (s *Source) Get(key string) (value string, isSet bool) {
if key == "" {
return "", false
}
// TODO v4 custom environment variable to set the secrets parent directory
// and not to set each secret file to a specific path
envKey := strings.ToUpper(key)
envKey = strings.ReplaceAll(envKey, "-", "_")
envKey += "_SECRETFILE" // TODO v4 change _SECRETFILE to _FILE
path := s.environ[envKey]
if path == "" {
path = filepath.Join(s.rootDirectory, key)
}
settings.HTTPProxy, err = s.readHTTPProxy()
if err != nil {
return settings, err
// Special file parsing
switch key {
// TODO timezone from /etc/localtime
case "openvpn_clientcrt", "openvpn_clientkey":
value, isSet, err := files.ReadPEMFile(path)
if err != nil {
s.warner.Warnf("skipping %s: parsing PEM: %s", path, err)
}
return value, isSet
case "wireguard_private_key":
privateKey := s.lazyLoadWireguardConf().PrivateKey
if privateKey != nil {
return *privateKey, true
} // else continue to read from individual secret file
case "wireguard_preshared_key":
preSharedKey := s.lazyLoadWireguardConf().PreSharedKey
if preSharedKey != nil {
return *preSharedKey, true
} // else continue to read from individual secret file
case "wireguard_addresses":
addresses := s.lazyLoadWireguardConf().Addresses
if addresses != nil {
return *addresses, true
} // else continue to read from individual secret file
case "wireguard_public_key":
return strPtrToStringIsSet(s.lazyLoadWireguardConf().PublicKey)
case "vpn_endpoint_ip":
return strPtrToStringIsSet(s.lazyLoadWireguardConf().EndpointIP)
case "vpn_endpoint_port":
return strPtrToStringIsSet(s.lazyLoadWireguardConf().EndpointPort)
}
settings.Shadowsocks, err = s.readShadowsocks()
value, isSet, err := files.ReadFromFile(path)
if err != nil {
return settings, err
s.warner.Warnf("skipping %s: reading file: %s", path, err)
}
return value, isSet
}
func (s *Source) KeyTransform(key string) string {
switch key {
// TODO v4 remove these irregular cases
case "OPENVPN_KEY":
return "openvpn_clientkey"
case "OPENVPN_CERT":
return "openvpn_clientcrt"
default:
key = strings.ToLower(key) // HTTPROXY_USER -> httpproxy_user
return key
}
settings.VPN.Wireguard, err = s.readWireguard()
if err != nil {
return settings, fmt.Errorf("reading Wireguard: %w", err)
}
return settings, nil
}

View File

@@ -0,0 +1,102 @@
package secrets
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_Source_Get(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
makeSource func(tempDir string) (source *Source, err error)
key string
value string
isSet bool
}{
"empty_key": {
makeSource: func(tempDir string) (source *Source, err error) {
return &Source{
rootDirectory: tempDir,
environ: map[string]string{},
}, nil
},
},
"no_secret_file": {
makeSource: func(tempDir string) (source *Source, err error) {
return &Source{
rootDirectory: tempDir,
environ: map[string]string{},
}, nil
},
key: "test_file",
},
"empty_secret_file": {
makeSource: func(tempDir string) (source *Source, err error) {
secretFilepath := filepath.Join(tempDir, "test_file")
err = os.WriteFile(secretFilepath, nil, os.ModePerm)
if err != nil {
return nil, err
}
return &Source{
rootDirectory: tempDir,
environ: map[string]string{},
}, nil
},
key: "test_file",
isSet: true,
},
"default_secret_file": {
makeSource: func(tempDir string) (source *Source, err error) {
secretFilepath := filepath.Join(tempDir, "test_file")
err = os.WriteFile(secretFilepath, []byte{'A'}, os.ModePerm)
if err != nil {
return nil, err
}
return &Source{
rootDirectory: tempDir,
environ: map[string]string{},
}, nil
},
key: "test_file",
value: "A",
isSet: true,
},
"env_specified_secret_file": {
makeSource: func(tempDir string) (source *Source, err error) {
secretFilepath := filepath.Join(tempDir, "test_file_custom")
err = os.WriteFile(secretFilepath, []byte{'A'}, os.ModePerm)
if err != nil {
return nil, err
}
return &Source{
rootDirectory: tempDir,
environ: map[string]string{
"TEST_FILE_SECRETFILE": secretFilepath,
},
}, nil
},
key: "test_file",
value: "A",
isSet: true,
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
source, err := testCase.makeSource(t.TempDir())
require.NoError(t, err)
value, isSet := source.Get(testCase.key)
assert.Equal(t, testCase.value, value)
assert.Equal(t, testCase.isSet, isSet)
})
}
}

View File

@@ -1,19 +0,0 @@
package secrets
import (
"fmt"
"github.com/qdm12/gluetun/internal/configuration/settings"
)
func (s *Source) readShadowsocks() (settings settings.Shadowsocks, err error) {
settings.Password, err = s.readSecretFileAsStringPtr(
"SHADOWSOCKS_PASSWORD_SECRETFILE",
"/run/secrets/shadowsocks_password",
)
if err != nil {
return settings, fmt.Errorf("reading Shadowsocks password secret file: %w", err)
}
return settings, nil
}

View File

@@ -1,21 +0,0 @@
package secrets
import (
"fmt"
"github.com/qdm12/gluetun/internal/configuration/settings"
)
func (s *Source) readVPN() (vpn settings.VPN, err error) {
vpn.OpenVPN, err = s.readOpenVPN()
if err != nil {
return vpn, fmt.Errorf("reading OpenVPN settings: %w", err)
}
vpn.Wireguard, err = s.readWireguard()
if err != nil {
return vpn, fmt.Errorf("reading Wireguard settings: %w", err)
}
return vpn, nil
}

View File

@@ -1,52 +1,27 @@
package secrets
import (
"fmt"
"os"
"path/filepath"
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/configuration/sources/files"
)
func (s *Source) readWireguard() (settings settings.Wireguard, err error) {
wireguardConf, err := s.readSecretFileAsStringPtr(
"WIREGUARD_CONF_SECRETFILE",
"/run/secrets/wg0.conf",
)
if err != nil {
return settings, fmt.Errorf("reading Wireguard conf secret file: %w", err)
} else if wireguardConf != nil {
// Wireguard ini config file takes precedence over individual secrets
return files.ParseWireguardConf([]byte(*wireguardConf))
func (s *Source) lazyLoadWireguardConf() files.WireguardConfig {
if s.cached.wireguardLoaded {
return s.cached.wireguardConf
}
settings.PrivateKey, err = s.readSecretFileAsStringPtr(
"WIREGUARD_PRIVATE_KEY_SECRETFILE",
"/run/secrets/wireguard_private_key",
)
if err != nil {
return settings, fmt.Errorf("reading private key file: %w", err)
path := os.Getenv("WIREGUARD_CONF_SECRETFILE")
if path == "" {
path = filepath.Join(s.rootDirectory, "wg0.conf")
}
settings.PreSharedKey, err = s.readSecretFileAsStringPtr(
"WIREGUARD_PRESHARED_KEY_SECRETFILE",
"/run/secrets/wireguard_preshared_key",
)
s.cached.wireguardLoaded = true
var err error
s.cached.wireguardConf, err = files.ParseWireguardConf(path)
if err != nil {
return settings, fmt.Errorf("reading preshared key file: %w", err)
s.warner.Warnf("skipping Wireguard config: %s", err)
}
wireguardAddressesCSV, err := s.readSecretFileAsStringPtr(
"WIREGUARD_ADDRESSES_SECRETFILE",
"/run/secrets/wireguard_addresses",
)
if err != nil {
return settings, fmt.Errorf("reading addresses file: %w", err)
} else if wireguardAddressesCSV != nil {
settings.Addresses, err = parseAddresses(*wireguardAddressesCSV)
if err != nil {
return settings, fmt.Errorf("parsing addresses: %w", err)
}
}
return settings, nil
return s.cached.wireguardConf
}