package settings import ( "fmt" "slices" "strings" "time" "github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gosettings" "github.com/qdm12/gosettings/reader" "github.com/qdm12/gosettings/validate" "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 the empty string in the internal state. DNSAddress string // MinRatio is the minimum ratio of servers to // find per provider, compared to the total current // number of servers. It defaults to 0.8. MinRatio float64 // Providers is the list of VPN service providers // to update server information for. Providers []string // ProtonEmail is the email to authenticate with the Proton API. ProtonEmail *string // ProtonPassword is the password to authenticate with the Proton API. ProtonPassword *string } 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) } if u.MinRatio <= 0 || u.MinRatio > 1 { return fmt.Errorf("%w: %.2f must be between 0+ and 1", ErrMinRatioNotValid, u.MinRatio) } validProviders := providers.All() for _, provider := range u.Providers { err = validate.IsOneOf(provider, validProviders...) if err != nil { return fmt.Errorf("%w: %w", ErrVPNProviderNameNotValid, err) } if provider == providers.Protonvpn { authenticatedAPI := *u.ProtonEmail != "" || *u.ProtonPassword != "" if authenticatedAPI { switch { case *u.ProtonEmail == "": return fmt.Errorf("%w", ErrUpdaterProtonEmailMissing) case *u.ProtonPassword == "": return fmt.Errorf("%w", ErrUpdaterProtonPasswordMissing) } } } } return nil } func (u *Updater) copy() (copied Updater) { return Updater{ Period: gosettings.CopyPointer(u.Period), DNSAddress: u.DNSAddress, MinRatio: u.MinRatio, Providers: gosettings.CopySlice(u.Providers), ProtonEmail: gosettings.CopyPointer(u.ProtonEmail), ProtonPassword: gosettings.CopyPointer(u.ProtonPassword), } } // overrideWith overrides fields of the receiver // settings object with any field set in the other // settings. func (u *Updater) overrideWith(other Updater) { u.Period = gosettings.OverrideWithPointer(u.Period, other.Period) u.DNSAddress = gosettings.OverrideWithComparable(u.DNSAddress, other.DNSAddress) u.MinRatio = gosettings.OverrideWithComparable(u.MinRatio, other.MinRatio) u.Providers = gosettings.OverrideWithSlice(u.Providers, other.Providers) u.ProtonEmail = gosettings.OverrideWithPointer(u.ProtonEmail, other.ProtonEmail) u.ProtonPassword = gosettings.OverrideWithPointer(u.ProtonPassword, other.ProtonPassword) } func (u *Updater) SetDefaults(vpnProvider string) { u.Period = gosettings.DefaultPointer(u.Period, 0) u.DNSAddress = gosettings.DefaultComparable(u.DNSAddress, "1.1.1.1:53") if u.MinRatio == 0 { const defaultMinRatio = 0.8 u.MinRatio = defaultMinRatio } if len(u.Providers) == 0 && vpnProvider != providers.Custom { u.Providers = []string{vpnProvider} } // Set these to empty strings to avoid nil pointer panics u.ProtonEmail = gosettings.DefaultPointer(u.ProtonEmail, "") u.ProtonPassword = gosettings.DefaultPointer(u.ProtonPassword, "") } func (u Updater) String() string { return u.toLinesNode().String() } func (u Updater) toLinesNode() (node *gotree.Node) { if *u.Period == 0 || len(u.Providers) == 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("Minimum ratio: %.1f", u.MinRatio) node.Appendf("Providers to update: %s", strings.Join(u.Providers, ", ")) if slices.Contains(u.Providers, providers.Protonvpn) { node.Appendf("Proton API email: %s", *u.ProtonEmail) node.Appendf("Proton API password: %s", gosettings.ObfuscateKey(*u.ProtonPassword)) } return node } func (u *Updater) read(r *reader.Reader) (err error) { u.Period, err = r.DurationPtr("UPDATER_PERIOD") if err != nil { return err } u.DNSAddress, err = readUpdaterDNSAddress() if err != nil { return err } u.MinRatio, err = r.Float64("UPDATER_MIN_RATIO") if err != nil { return err } u.Providers = r.CSV("UPDATER_VPN_SERVICE_PROVIDERS") u.ProtonEmail = r.Get("UPDATER_PROTONVPN_EMAIL") if u.ProtonEmail == nil { protonUsername := r.String("UPDATER_PROTONVPN_USERNAME", reader.IsRetro("UPDATER_PROTONVPN_EMAIL")) if protonUsername != "" { protonEmail := protonUsername + "@protonmail.com" u.ProtonEmail = &protonEmail } } u.ProtonPassword = r.Get("UPDATER_PROTONVPN_PASSWORD") return 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 }