Merge branch 'master' into nftables

This commit is contained in:
Quentin McGaw
2026-08-06 20:13:34 +00:00
491 changed files with 22568 additions and 314322 deletions
@@ -0,0 +1,234 @@
package settings
import (
"fmt"
"strconv"
"strings"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
)
type AmneziaWg struct {
// Wireguard contains the configuration for Wireguard, given
// AmneziaWg is based on Wireguard
Wireguard Wireguard `json:"wireguard"`
JunkPacketCount *uint16 `json:"junk_packet_count"`
JunkPacketMin *uint16 `json:"junk_packet_min"`
JunkPacketMax *uint16 `json:"junk_packet_max"`
PaddingS1 *uint16 `json:"padding_s1"`
PaddingS2 *uint16 `json:"padding_s2"`
PaddingS3 *uint16 `json:"padding_s3"`
PaddingS4 *uint16 `json:"padding_s4"`
HeaderH1 *string `json:"header_h1"`
HeaderH2 *string `json:"header_h2"`
HeaderH3 *string `json:"header_h3"`
HeaderH4 *string `json:"header_h4"`
InitPacketI1 *string `json:"init_packet_i1"`
InitPacketI2 *string `json:"init_packet_i2"`
InitPacketI3 *string `json:"init_packet_i3"`
InitPacketI4 *string `json:"init_packet_i4"`
InitPacketI5 *string `json:"init_packet_i5"`
}
func (a *AmneziaWg) read(r *reader.Reader) (err error) {
const amneziawg = true
err = a.Wireguard.read(r, amneziawg)
if err != nil {
return err // do not wrap this error
}
uint16Fields := map[string]**uint16{
"AMNEZIAWG_JC": &a.JunkPacketCount,
"AMNEZIAWG_JMIN": &a.JunkPacketMin,
"AMNEZIAWG_JMAX": &a.JunkPacketMax,
"AMNEZIAWG_S1": &a.PaddingS1,
"AMNEZIAWG_S2": &a.PaddingS2,
"AMNEZIAWG_S3": &a.PaddingS3,
"AMNEZIAWG_S4": &a.PaddingS4,
}
for key, dst := range uint16Fields {
*dst, err = r.Uint16Ptr(key)
if err != nil {
return err
}
}
stringFields := map[string]**string{
"AMNEZIAWG_H1": &a.HeaderH1,
"AMNEZIAWG_H2": &a.HeaderH2,
"AMNEZIAWG_H3": &a.HeaderH3,
"AMNEZIAWG_H4": &a.HeaderH4,
"AMNEZIAWG_I1": &a.InitPacketI1,
"AMNEZIAWG_I2": &a.InitPacketI2,
"AMNEZIAWG_I3": &a.InitPacketI3,
"AMNEZIAWG_I4": &a.InitPacketI4,
"AMNEZIAWG_I5": &a.InitPacketI5,
}
opt := reader.ForceLowercase(false)
for key, dst := range stringFields {
*dst = r.Get(key, opt)
}
return nil
}
func (a AmneziaWg) copy() (copied AmneziaWg) {
return AmneziaWg{
Wireguard: a.Wireguard.copy(),
JunkPacketCount: gosettings.CopyPointer(a.JunkPacketCount),
JunkPacketMin: gosettings.CopyPointer(a.JunkPacketMin),
JunkPacketMax: gosettings.CopyPointer(a.JunkPacketMax),
PaddingS1: gosettings.CopyPointer(a.PaddingS1),
PaddingS2: gosettings.CopyPointer(a.PaddingS2),
PaddingS3: gosettings.CopyPointer(a.PaddingS3),
PaddingS4: gosettings.CopyPointer(a.PaddingS4),
HeaderH1: gosettings.CopyPointer(a.HeaderH1),
HeaderH2: gosettings.CopyPointer(a.HeaderH2),
HeaderH3: gosettings.CopyPointer(a.HeaderH3),
HeaderH4: gosettings.CopyPointer(a.HeaderH4),
InitPacketI1: gosettings.CopyPointer(a.InitPacketI1),
InitPacketI2: gosettings.CopyPointer(a.InitPacketI2),
InitPacketI3: gosettings.CopyPointer(a.InitPacketI3),
InitPacketI4: gosettings.CopyPointer(a.InitPacketI4),
InitPacketI5: gosettings.CopyPointer(a.InitPacketI5),
}
}
func (a *AmneziaWg) overrideWith(other AmneziaWg) {
a.Wireguard.overrideWith(other.Wireguard)
a.JunkPacketCount = gosettings.OverrideWithPointer(a.JunkPacketCount, other.JunkPacketCount)
a.JunkPacketMin = gosettings.OverrideWithPointer(a.JunkPacketMin, other.JunkPacketMin)
a.JunkPacketMax = gosettings.OverrideWithPointer(a.JunkPacketMax, other.JunkPacketMax)
a.PaddingS1 = gosettings.OverrideWithPointer(a.PaddingS1, other.PaddingS1)
a.PaddingS2 = gosettings.OverrideWithPointer(a.PaddingS2, other.PaddingS2)
a.PaddingS3 = gosettings.OverrideWithPointer(a.PaddingS3, other.PaddingS3)
a.PaddingS4 = gosettings.OverrideWithPointer(a.PaddingS4, other.PaddingS4)
a.HeaderH1 = gosettings.OverrideWithPointer(a.HeaderH1, other.HeaderH1)
a.HeaderH2 = gosettings.OverrideWithPointer(a.HeaderH2, other.HeaderH2)
a.HeaderH3 = gosettings.OverrideWithPointer(a.HeaderH3, other.HeaderH3)
a.HeaderH4 = gosettings.OverrideWithPointer(a.HeaderH4, other.HeaderH4)
a.InitPacketI1 = gosettings.OverrideWithPointer(a.InitPacketI1, other.InitPacketI1)
a.InitPacketI2 = gosettings.OverrideWithPointer(a.InitPacketI2, other.InitPacketI2)
a.InitPacketI3 = gosettings.OverrideWithPointer(a.InitPacketI3, other.InitPacketI3)
a.InitPacketI4 = gosettings.OverrideWithPointer(a.InitPacketI4, other.InitPacketI4)
a.InitPacketI5 = gosettings.OverrideWithPointer(a.InitPacketI5, other.InitPacketI5)
}
func (a *AmneziaWg) setDefaults(vpnProvider string) {
a.Wireguard.setDefaults(vpnProvider)
a.Wireguard.Implementation = "userspace" // unused except in logs
a.JunkPacketCount = gosettings.DefaultPointer(a.JunkPacketCount, 0)
a.JunkPacketMin = gosettings.DefaultPointer(a.JunkPacketMin, 0)
a.JunkPacketMax = gosettings.DefaultPointer(a.JunkPacketMax, 0)
a.PaddingS1 = gosettings.DefaultPointer(a.PaddingS1, 0)
a.PaddingS2 = gosettings.DefaultPointer(a.PaddingS2, 0)
a.PaddingS3 = gosettings.DefaultPointer(a.PaddingS3, 0)
a.PaddingS4 = gosettings.DefaultPointer(a.PaddingS4, 0)
a.HeaderH1 = gosettings.DefaultPointer(a.HeaderH1, "")
a.HeaderH2 = gosettings.DefaultPointer(a.HeaderH2, "")
a.HeaderH3 = gosettings.DefaultPointer(a.HeaderH3, "")
a.HeaderH4 = gosettings.DefaultPointer(a.HeaderH4, "")
a.InitPacketI1 = gosettings.DefaultPointer(a.InitPacketI1, "")
a.InitPacketI2 = gosettings.DefaultPointer(a.InitPacketI2, "")
a.InitPacketI3 = gosettings.DefaultPointer(a.InitPacketI3, "")
a.InitPacketI4 = gosettings.DefaultPointer(a.InitPacketI4, "")
a.InitPacketI5 = gosettings.DefaultPointer(a.InitPacketI5, "")
}
func (a AmneziaWg) toLinesNode() (node *gotree.Node) {
node = gotree.New("AmneziaWG settings:")
node.AppendNode(a.Wireguard.toLinesNode())
uintFields := []struct {
key string
val *uint16
}{
{"JC", a.JunkPacketCount},
{"JMIN", a.JunkPacketMin},
{"JMAX", a.JunkPacketMax},
{"S1", a.PaddingS1},
{"S2", a.PaddingS2},
{"S3", a.PaddingS3},
{"S4", a.PaddingS4},
}
for _, f := range uintFields {
node.Appendf("%s: %d", f.key, *f.val)
}
stringFields := []struct {
key string
val *string
}{
{"H1", a.HeaderH1},
{"H2", a.HeaderH2},
{"H3", a.HeaderH3},
{"H4", a.HeaderH4},
{"I1", a.InitPacketI1},
{"I2", a.InitPacketI2},
{"I3", a.InitPacketI3},
{"I4", a.InitPacketI4},
{"I5", a.InitPacketI5},
}
for _, f := range stringFields {
node.Appendf("%s: %s", f.key, *f.val)
}
return node
}
func (a AmneziaWg) validate(vpnProvider string, ipv6Supported bool) error {
const amneziaWG = true
err := a.Wireguard.validate(vpnProvider, ipv6Supported, amneziaWG)
if err != nil {
return fmt.Errorf("wireguard settings: %w", err)
}
if *a.JunkPacketCount == 0 {
if *a.JunkPacketMin != 0 || *a.JunkPacketMax != 0 {
return fmt.Errorf("junk packet count must be set when junk packet min or max is set: "+
"jc=%d and jmin=%d and jmax=%d", a.JunkPacketCount, *a.JunkPacketMin, *a.JunkPacketMax)
}
} else {
if *a.JunkPacketMin == 0 || *a.JunkPacketMax == 0 {
return fmt.Errorf("junk packet min and max must be set when junk packet count is set: "+
"jc=%d and jmin=%d and jmax=%d", a.JunkPacketCount, *a.JunkPacketMin, *a.JunkPacketMax)
} else if *a.JunkPacketMin > *a.JunkPacketMax {
return fmt.Errorf("junk packet minimum must be lower than or equal to maximum: "+
"jmin=%d and jmax=%d", *a.JunkPacketMin, *a.JunkPacketMax)
}
}
nameToHeaderRange := map[string]string{
"h1": *a.HeaderH1,
"h2": *a.HeaderH2,
"h3": *a.HeaderH3,
"h4": *a.HeaderH4,
}
for name, headerRange := range nameToHeaderRange {
if headerRange == "" {
continue
}
fields := strings.Split(headerRange, "-")
switch len(fields) {
case 1:
_, err := strconv.Atoi(fields[0])
if err != nil {
return fmt.Errorf("header range is malformed: "+
"%s value %s is not a number", name, headerRange)
}
case 2: //nolint:mnd
for _, field := range fields {
_, err := strconv.Atoi(field)
if err != nil {
return fmt.Errorf("header range is malformed: "+
"%s value %s is not a valid range", name, headerRange)
}
}
default:
return fmt.Errorf("header range is malformed: "+
"%s value %s must be in the form n or n-m", name, headerRange)
}
}
return nil
}
@@ -0,0 +1,51 @@
package settings
import (
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
)
type BoringPoll struct {
GluetunCom *bool
}
func (b BoringPoll) validate() error {
return nil
}
func (b BoringPoll) Copy() BoringPoll {
return BoringPoll{
GluetunCom: gosettings.CopyPointer(b.GluetunCom),
}
}
func (b *BoringPoll) overrideWith(other BoringPoll) {
b.GluetunCom = gosettings.OverrideWithPointer(b.GluetunCom, other.GluetunCom)
}
func (b *BoringPoll) setDefaults() {
b.GluetunCom = gosettings.DefaultPointer(b.GluetunCom, false)
}
func (b BoringPoll) String() string {
return b.toLinesNode().String()
}
func (b BoringPoll) toLinesNode() *gotree.Node {
if !*b.GluetunCom {
return nil
}
node := gotree.New("Boring-poll settings:")
node.Append("gluetun.com: on")
return node
}
func (b *BoringPoll) read(r *reader.Reader) (err error) {
b.GluetunCom, err = r.BoolPtr("BORINGPOLL_GLUETUNCOM")
if err != nil {
return err
}
return nil
}
@@ -1,10 +1,10 @@
package settings
import (
"maps"
"slices"
"github.com/qdm12/gosettings/reader"
"golang.org/x/exp/maps"
)
func readObsolete(r *reader.Reader) (warnings []string) {
@@ -14,8 +14,11 @@ func readObsolete(r *reader.Reader) (warnings []string) {
"DOT_VALIDATION_LOGLEVEL": "DOT_VALIDATION_LOGLEVEL is obsolete because DNSSEC validation is not implemented.",
"HEALTH_VPN_DURATION_INITIAL": "HEALTH_VPN_DURATION_INITIAL is obsolete",
"HEALTH_VPN_DURATION_ADDITION": "HEALTH_VPN_DURATION_ADDITION is obsolete",
"DNS_KEEP_NAMESERVER": "DNS_KEEP_NAMESERVER is obsolete because you should use the built-in server which now " +
"forwards local names to private DNS resolvers found in /etc/resolv.conf at container start",
"BLOCK_SURVEILLANCE": "BLOCK_SURVEILLANCE is obsolete because its DNS block lists are not longer maintained",
}
sortedKeys := maps.Keys(keyToMessage)
sortedKeys := slices.Collect(maps.Keys(keyToMessage))
slices.Sort(sortedKeys)
warnings = make([]string, 0, len(keyToMessage))
for _, key := range sortedKeys {
+141 -86
View File
@@ -1,7 +1,6 @@
package settings
import (
"errors"
"fmt"
"net/netip"
"time"
@@ -13,20 +12,28 @@ import (
"github.com/qdm12/gotree"
)
const (
DNSUpstreamTypeDot = "dot"
DNSUpstreamTypeDoh = "doh"
DNSUpstreamTypePlain = "plain"
)
// DNS contains settings to configure DNS.
type DNS struct {
// ServerEnabled is true if the server should be running
// and used. It defaults to true, and cannot be nil
// in the internal state.
ServerEnabled *bool
// UpstreamType can be dot or plain, and defaults to dot.
// ServerEnabled indicates if the DNS server should be enabled.
// It defaults to true and cannot be nil in the internal state.
ServerEnabled *bool `json:"enabled"`
// UpstreamType can be [DNSUpstreamTypeDot], [DNSUpstreamTypeDoh]
// or [DNSUpstreamTypePlain]. It defaults to [DNSUpstreamTypeDot].
UpstreamType string `json:"upstream_type"`
// UpdatePeriod is the period to update DNS block lists.
// 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
// Providers is a list of DNS providers
// Providers is a list of DNS providers.
// It defaults to ["cloudflare"] and is ignored if the UpstreamType is
// [DNSUpstreamTypePlain] and the UpstreamPlainAddresses field is set.
Providers []string `json:"providers"`
// Caching is true if the server should cache
// DNS responses.
@@ -36,49 +43,55 @@ type DNS struct {
// Blacklist contains settings to configure the filter
// block lists.
Blacklist DNSBlacklist
// 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
// local server. It cannot be the zero value in the internal
// state.
ServerAddress netip.Addr
// KeepNameserver is true if the existing DNS server
// found in /etc/resolv.conf should be used
// Note setting this to true will likely DNS traffic
// outside the VPN tunnel since it would go through
// the local DNS server of your Docker/Kubernetes
// configuration, which is likely not going through the tunnel.
// This will also disable the DNS forwarder server and the
// `ServerAddress` field will be ignored.
// It defaults to false and cannot be nil in the
// internal state.
KeepNameserver *bool
// UpstreamPlainAddresses are the upstream plaintext DNS resolver
// addresses to use by the built-in DNS server forwarder.
// Note, if the upstream type is [dnsUpstreamTypePlain] and this field is set,
// the Providers field is ignored.
UpstreamPlainAddresses []netip.AddrPort
}
var (
ErrDNSUpstreamTypeNotValid = errors.New("DNS upstream type is not valid")
ErrDNSUpdatePeriodTooShort = errors.New("update period is too short")
)
func (d DNS) validate() (err error) {
if !helpers.IsOneOf(d.UpstreamType, "dot", "doh", "plain") {
return fmt.Errorf("%w: %s", ErrDNSUpstreamTypeNotValid, d.UpstreamType)
if !helpers.IsOneOf(d.UpstreamType, DNSUpstreamTypeDot, DNSUpstreamTypeDoh, DNSUpstreamTypePlain) {
return fmt.Errorf("DNS upstream type is not valid: %s", d.UpstreamType)
}
const minUpdatePeriod = 30 * time.Second
if *d.UpdatePeriod != 0 && *d.UpdatePeriod < minUpdatePeriod {
return fmt.Errorf("%w: %s must be bigger than %s",
ErrDNSUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod)
}
providers := provider.NewProviders()
for _, providerName := range d.Providers {
_, err := providers.Get(providerName)
if !*d.ServerEnabled {
err = d.validateForServerOff()
if err != nil {
return err
}
}
const minUpdatePeriod = 30 * time.Second
if *d.UpdatePeriod != 0 && *d.UpdatePeriod < minUpdatePeriod {
return fmt.Errorf("update period is too short: %s must be bigger than %s",
*d.UpdatePeriod, minUpdatePeriod)
}
if d.UpstreamType == DNSUpstreamTypePlain {
selectedHasPlainIPv4, selectedHasPlainIPv6 := false, false
for _, addrPort := range d.UpstreamPlainAddresses {
if !selectedHasPlainIPv4 && addrPort.Addr().Is4() {
selectedHasPlainIPv4 = true
}
if !selectedHasPlainIPv6 && addrPort.Addr().Is6() {
selectedHasPlainIPv6 = true
}
if selectedHasPlainIPv4 && selectedHasPlainIPv6 {
break
}
}
switch {
case *d.IPv6 && !selectedHasPlainIPv6:
return fmt.Errorf("upstream plain addresses do not contain any IPv6 address: "+
"in %d addresses", len(d.UpstreamPlainAddresses))
case !*d.IPv6 && !selectedHasPlainIPv4:
return fmt.Errorf("upstream plain addresses do not contain any IPv4 address: "+
"in %d addresses", len(d.UpstreamPlainAddresses))
}
}
// Note: all DNS built in providers have both IPv4 and IPv6 addresses for all modes
err = d.Blacklist.validate()
if err != nil {
return err
@@ -87,17 +100,33 @@ func (d DNS) validate() (err error) {
return nil
}
func (d DNS) validateForServerOff() (err error) {
switch {
case d.UpstreamType != DNSUpstreamTypePlain:
return fmt.Errorf("upstream type %s must be %s if the built-in DNS server is disabled",
d.UpstreamType, DNSUpstreamTypePlain)
case len(d.UpstreamPlainAddresses) == 0:
return fmt.Errorf("if DNS is disabled, at least one upstream plain address must be set")
}
for _, addrPort := range d.UpstreamPlainAddresses {
const defaultDNSPort = 53
if addrPort.Port() != defaultDNSPort {
return fmt.Errorf("invalid DNS port in %s: must be %d", addrPort, defaultDNSPort)
}
}
return nil
}
func (d *DNS) Copy() (copied DNS) {
return DNS{
ServerEnabled: gosettings.CopyPointer(d.ServerEnabled),
UpstreamType: d.UpstreamType,
UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod),
Providers: gosettings.CopySlice(d.Providers),
Caching: gosettings.CopyPointer(d.Caching),
IPv6: gosettings.CopyPointer(d.IPv6),
Blacklist: d.Blacklist.copy(),
ServerAddress: d.ServerAddress,
KeepNameserver: gosettings.CopyPointer(d.KeepNameserver),
ServerEnabled: gosettings.CopyPointer(d.ServerEnabled),
UpstreamType: d.UpstreamType,
UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod),
Providers: gosettings.CopySlice(d.Providers),
Caching: gosettings.CopyPointer(d.Caching),
IPv6: gosettings.CopyPointer(d.IPv6),
Blacklist: d.Blacklist.copy(),
UpstreamPlainAddresses: gosettings.CopySlice(d.UpstreamPlainAddresses),
}
}
@@ -112,41 +141,29 @@ func (d *DNS) overrideWith(other DNS) {
d.Caching = gosettings.OverrideWithPointer(d.Caching, other.Caching)
d.IPv6 = gosettings.OverrideWithPointer(d.IPv6, other.IPv6)
d.Blacklist.overrideWith(other.Blacklist)
d.ServerAddress = gosettings.OverrideWithValidator(d.ServerAddress, other.ServerAddress)
d.KeepNameserver = gosettings.OverrideWithPointer(d.KeepNameserver, other.KeepNameserver)
d.UpstreamPlainAddresses = gosettings.OverrideWithSlice(d.UpstreamPlainAddresses, other.UpstreamPlainAddresses)
}
func (d *DNS) setDefaults() {
d.ServerEnabled = gosettings.DefaultPointer(d.ServerEnabled, true)
d.UpstreamType = gosettings.DefaultComparable(d.UpstreamType, "dot")
defaultUpstreamType := DNSUpstreamTypeDot
if !*d.ServerEnabled {
defaultUpstreamType = DNSUpstreamTypePlain
}
d.UpstreamType = gosettings.DefaultComparable(d.UpstreamType, defaultUpstreamType)
const defaultUpdatePeriod = 24 * time.Hour
d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod)
d.Providers = gosettings.DefaultSlice(d.Providers, []string{
provider.Cloudflare().Name,
})
d.UpstreamPlainAddresses = gosettings.DefaultSlice(d.UpstreamPlainAddresses, []netip.AddrPort{})
d.Providers = gosettings.DefaultSlice(d.Providers, defaultDNSProviders())
d.Caching = gosettings.DefaultPointer(d.Caching, true)
d.IPv6 = gosettings.DefaultPointer(d.IPv6, false)
d.Blacklist.setDefaults()
d.ServerAddress = gosettings.DefaultValidator(d.ServerAddress,
netip.AddrFrom4([4]byte{127, 0, 0, 1}))
d.KeepNameserver = gosettings.DefaultPointer(d.KeepNameserver, false)
}
func (d DNS) GetFirstPlaintextIPv4() (ipv4 netip.Addr) {
localhost := netip.AddrFrom4([4]byte{127, 0, 0, 1})
if d.ServerAddress.Compare(localhost) != 0 && d.ServerAddress.Is4() {
return d.ServerAddress
func defaultDNSProviders() []string {
return []string{
provider.Cloudflare().Name,
}
providers := provider.NewProviders()
provider, err := providers.Get(d.Providers[0])
if err != nil {
// Settings should be validated before calling this function,
// so an error happening here is a programming error.
panic(err)
}
return provider.Plain.IPv4[0].Addr()
}
func (d DNS) String() string {
@@ -155,22 +172,33 @@ func (d DNS) String() string {
func (d DNS) toLinesNode() (node *gotree.Node) {
node = gotree.New("DNS settings:")
node.Appendf("Keep existing nameserver(s): %s", gosettings.BoolToYesNo(d.KeepNameserver))
if *d.KeepNameserver {
return node
}
node.Appendf("DNS server address to use: %s", d.ServerAddress)
node.Appendf("DNS forwarder server enabled: %s", gosettings.BoolToYesNo(d.ServerEnabled))
if !*d.ServerEnabled {
plainServers := node.Append("Plain DNS servers to use directly:")
for _, addr := range d.UpstreamPlainAddresses {
plainServers.Append(addr.String())
}
return node
}
node.Appendf("Upstream resolver type: %s", d.UpstreamType)
upstreamResolvers := node.Append("Upstream resolvers:")
for _, provider := range d.Providers {
upstreamResolvers.Append(provider)
if len(d.UpstreamPlainAddresses) > 0 {
if d.UpstreamType == DNSUpstreamTypePlain {
for _, addr := range d.UpstreamPlainAddresses {
upstreamResolvers.Append(addr.String())
}
} else {
node.Appendf("Upstream plain addresses: ignored because upstream type is not plain")
for _, provider := range d.Providers {
upstreamResolvers.Append(provider)
}
}
} else {
for _, provider := range d.Providers {
upstreamResolvers.Append(provider)
}
}
node.Appendf("Caching: %s", gosettings.BoolToYesNo(d.Caching))
@@ -217,15 +245,42 @@ func (d *DNS) read(r *reader.Reader) (err error) {
return err
}
d.ServerAddress, err = r.NetipAddr("DNS_ADDRESS", reader.RetroKeys("DNS_PLAINTEXT_ADDRESS"))
if err != nil {
return err
}
d.KeepNameserver, err = r.BoolPtr("DNS_KEEP_NAMESERVER")
err = d.readUpstreamPlainAddresses(r)
if err != nil {
return err
}
return nil
}
func (d *DNS) readUpstreamPlainAddresses(r *reader.Reader) (err error) {
// If DNS_UPSTREAM_PLAIN_ADDRESSES is set, the user must also set DNS_UPSTREAM_RESOLVER_TYPE=plain
// for these to be used. This is an added safety measure to reduce misunderstandings, and
// reduce odd settings overrides.
d.UpstreamPlainAddresses, err = r.CSVNetipAddrPorts("DNS_UPSTREAM_PLAIN_ADDRESSES")
if err != nil {
return err
}
// Retro-compatibility - remove in v4
// If DNS_ADDRESS is set to a non-localhost address, append it to the other
// upstream plain addresses, assuming port 53, and force the upstream type to plain
// to maintain retro-compatibility behavior.
serverAddress, err := r.NetipAddr("DNS_ADDRESS",
reader.RetroKeys("DNS_PLAINTEXT_ADDRESS"),
reader.IsRetro("DNS_UPSTREAM_PLAIN_ADDRESSES"))
if err != nil {
return err
} else if !serverAddress.IsValid() {
return nil
}
isLocalhost := serverAddress.Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) == 0
if isLocalhost {
return nil
}
const defaultPlainPort = 53
addrPort := netip.AddrPortFrom(serverAddress, defaultPlainPort)
d.UpstreamPlainAddresses = append(d.UpstreamPlainAddresses, addrPort)
d.UpstreamType = DNSUpstreamTypePlain
return nil
}
@@ -0,0 +1,26 @@
package settings
import (
"testing"
"github.com/qdm12/dns/v2/pkg/provider"
"github.com/stretchr/testify/require"
)
func Test_defaultDNSProviders(t *testing.T) {
t.Parallel()
names := defaultDNSProviders()
found := false
providers := provider.NewProviders()
for _, name := range names {
provider, err := providers.Get(name)
require.NoError(t, err)
if len(provider.Plain.IPv4) > 0 {
found = true
break
}
}
require.True(t, found, "no default DNS provider has a plaintext IPv4 address")
}
@@ -1,7 +1,6 @@
package settings
import (
"errors"
"fmt"
"net/http"
"net/netip"
@@ -17,7 +16,6 @@ import (
type DNSBlacklist struct {
BlockMalicious *bool
BlockAds *bool
BlockSurveillance *bool
AllowedHosts []string
AddBlockedHosts []string
AddBlockedIPs []netip.Addr
@@ -32,27 +30,20 @@ type DNSBlacklist struct {
func (b *DNSBlacklist) setDefaults() {
b.BlockMalicious = gosettings.DefaultPointer(b.BlockMalicious, true)
b.BlockAds = gosettings.DefaultPointer(b.BlockAds, false)
b.BlockSurveillance = gosettings.DefaultPointer(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")
ErrRebindingProtectionExemptHostNotValid = errors.New("rebinding protection exempt 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)
return fmt.Errorf("allowed host is not valid: %s", host)
}
}
for _, host := range b.AddBlockedHosts {
if !hostRegex.MatchString(host) {
return fmt.Errorf("%w: %s", ErrBlockedHostNotValid, host)
return fmt.Errorf("blocked host is not valid: %s", host)
}
}
@@ -61,7 +52,7 @@ func (b DNSBlacklist) validate() (err error) {
host = host[2:]
}
if !hostRegex.MatchString(host) {
return fmt.Errorf("%w: %s", ErrRebindingProtectionExemptHostNotValid, host)
return fmt.Errorf("rebinding protection exempt host is not valid: %s", host)
}
}
@@ -72,7 +63,6 @@ func (b DNSBlacklist) copy() (copied DNSBlacklist) {
return DNSBlacklist{
BlockMalicious: gosettings.CopyPointer(b.BlockMalicious),
BlockAds: gosettings.CopyPointer(b.BlockAds),
BlockSurveillance: gosettings.CopyPointer(b.BlockSurveillance),
AllowedHosts: gosettings.CopySlice(b.AllowedHosts),
AddBlockedHosts: gosettings.CopySlice(b.AddBlockedHosts),
AddBlockedIPs: gosettings.CopySlice(b.AddBlockedIPs),
@@ -84,7 +74,6 @@ func (b DNSBlacklist) copy() (copied DNSBlacklist) {
func (b *DNSBlacklist) overrideWith(other DNSBlacklist) {
b.BlockMalicious = gosettings.OverrideWithPointer(b.BlockMalicious, other.BlockMalicious)
b.BlockAds = gosettings.OverrideWithPointer(b.BlockAds, other.BlockAds)
b.BlockSurveillance = gosettings.OverrideWithPointer(b.BlockSurveillance, other.BlockSurveillance)
b.AllowedHosts = gosettings.OverrideWithSlice(b.AllowedHosts, other.AllowedHosts)
b.AddBlockedHosts = gosettings.OverrideWithSlice(b.AddBlockedHosts, other.AddBlockedHosts)
b.AddBlockedIPs = gosettings.OverrideWithSlice(b.AddBlockedIPs, other.AddBlockedIPs)
@@ -100,7 +89,6 @@ func (b DNSBlacklist) ToBlockBuilderSettings(client *http.Client) (
Client: client,
BlockMalicious: b.BlockMalicious,
BlockAds: b.BlockAds,
BlockSurveillance: b.BlockSurveillance,
AllowedHosts: b.AllowedHosts,
AddBlockedHosts: b.AddBlockedHosts,
AddBlockedIPs: b.AddBlockedIPs,
@@ -117,7 +105,6 @@ func (b DNSBlacklist) toLinesNode() (node *gotree.Node) {
node.Appendf("Block malicious: %s", gosettings.BoolToYesNo(b.BlockMalicious))
node.Appendf("Block ads: %s", gosettings.BoolToYesNo(b.BlockAds))
node.Appendf("Block surveillance: %s", gosettings.BoolToYesNo(b.BlockSurveillance))
if len(b.AllowedHosts) > 0 {
allowedHostsNode := node.Append("Allowed hosts:")
@@ -163,12 +150,6 @@ func (b *DNSBlacklist) read(r *reader.Reader) (err error) {
return err
}
b.BlockSurveillance, err = r.BoolPtr("BLOCK_SURVEILLANCE",
reader.RetroKeys("BLOCK_NSA"))
if err != nil {
return err
}
b.BlockAds, err = r.BoolPtr("BLOCK_ADS")
if err != nil {
return err
@@ -209,8 +190,6 @@ func readDNSBlockedIPs(r *reader.Reader) (ips []netip.Addr,
return ips, ipPrefixes, nil
}
var ErrPrivateAddressNotValid = errors.New("private address is not a valid IP or CIDR range")
func readDNSPrivateAddresses(r *reader.Reader) (ips []netip.Addr,
ipPrefixes []netip.Prefix, err error,
) {
@@ -236,8 +215,9 @@ func readDNSPrivateAddresses(r *reader.Reader) (ips []netip.Addr,
}
return nil, nil, fmt.Errorf(
"environment variable DOT_PRIVATE_ADDRESS: %w: %s",
ErrPrivateAddressNotValid, privateAddress)
"environment variable DOT_PRIVATE_ADDRESS: "+
"private address is not a valid IP or CIDR range: %s",
privateAddress)
}
return ips, ipPrefixes, nil
-58
View File
@@ -1,58 +0,0 @@
package settings
import "errors"
var (
ErrValueUnknown = errors.New("value is unknown")
ErrCityNotValid = errors.New("the city specified is not valid")
ErrControlServerPrivilegedPort = errors.New("cannot use privileged port without running as root")
ErrCategoryNotValid = errors.New("the category specified is not valid")
ErrCountryNotValid = errors.New("the country specified is not valid")
ErrFilepathMissing = errors.New("filepath is missing")
ErrFirewallZeroPort = errors.New("cannot have a zero port")
ErrFirewallPublicOutboundSubnet = errors.New("outbound subnet has an unspecified address")
ErrHostnameNotValid = errors.New("the hostname specified is not valid")
ErrISPNotValid = errors.New("the ISP specified is not valid")
ErrMinRatioNotValid = errors.New("minimum ratio is not valid")
ErrMissingValue = errors.New("missing value")
ErrNameNotValid = errors.New("the server name specified is not valid")
ErrOpenVPNClientKeyMissing = errors.New("client key is missing")
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")
ErrOpenVPNKeyPassphraseIsEmpty = errors.New("key passphrase is empty")
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")
ErrPortForwardingUserEmpty = errors.New("port forwarding username is empty")
ErrPortForwardingPasswordEmpty = errors.New("port forwarding password is empty")
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")
ErrUpdaterPeriodTooSmall = errors.New("VPN server data updater period is too small")
ErrUpdaterProtonPasswordMissing = errors.New("proton password is missing")
ErrUpdaterProtonEmailMissing = errors.New("proton email is missing")
ErrVPNProviderNameNotValid = errors.New("VPN provider name is not valid")
ErrVPNTypeNotValid = errors.New("VPN type is not valid")
ErrWireguardAllowedIPNotSet = errors.New("allowed IP is not set")
ErrWireguardAllowedIPsNotSet = errors.New("allowed IPs is not set")
ErrWireguardEndpointIPNotSet = errors.New("endpoint IP is not set")
ErrWireguardEndpointPortNotAllowed = errors.New("endpoint port is not allowed")
ErrWireguardEndpointPortNotSet = errors.New("endpoint port is not set")
ErrWireguardEndpointPortSet = errors.New("endpoint port is set")
ErrWireguardInterfaceAddressNotSet = errors.New("interface address is not set")
ErrWireguardInterfaceAddressIPv6 = errors.New("interface address is IPv6 but IPv6 is not supported")
ErrWireguardInterfaceNotValid = errors.New("interface name is not valid")
ErrWireguardPreSharedKeyNotSet = errors.New("pre-shared key is not set")
ErrWireguardPrivateKeyNotSet = errors.New("private key is not set")
ErrWireguardPublicKeyNotSet = errors.New("public key is not set")
ErrWireguardPublicKeyNotValid = errors.New("public key is not valid")
ErrWireguardKeepAliveNegative = errors.New("persistent keep alive interval is negative")
ErrWireguardImplementationNotValid = errors.New("implementation is not valid")
)
+17 -14
View File
@@ -1,6 +1,7 @@
package settings
import (
"errors"
"fmt"
"net/netip"
@@ -16,22 +17,22 @@ type Firewall struct {
InputPorts []uint16
OutboundSubnets []netip.Prefix
Enabled *bool
Debug *bool
Implementation string
Iptables Iptables
}
func (f Firewall) validate() (err error) {
if hasZeroPort(f.VPNInputPorts) {
return fmt.Errorf("VPN input ports: %w", ErrFirewallZeroPort)
return errors.New("VPN input ports: cannot have a zero port")
}
if hasZeroPort(f.InputPorts) {
return fmt.Errorf("input ports: %w", ErrFirewallZeroPort)
return errors.New("input ports: cannot have a zero port")
}
for _, subnet := range f.OutboundSubnets {
if subnet.Addr().IsUnspecified() {
return fmt.Errorf("%w: %s", ErrFirewallPublicOutboundSubnet, subnet)
return fmt.Errorf("outbound subnet has an unspecified address: %s", subnet)
}
}
@@ -40,6 +41,11 @@ func (f Firewall) validate() (err error) {
return fmt.Errorf("firewall implementation: %w", err)
}
err = f.Iptables.validate()
if err != nil {
return fmt.Errorf("iptables settings: %w", err)
}
return nil
}
@@ -58,8 +64,8 @@ func (f *Firewall) copy() (copied Firewall) {
InputPorts: gosettings.CopySlice(f.InputPorts),
OutboundSubnets: gosettings.CopySlice(f.OutboundSubnets),
Enabled: gosettings.CopyPointer(f.Enabled),
Debug: gosettings.CopyPointer(f.Debug),
Implementation: f.Implementation,
Iptables: f.Iptables.copy(),
}
}
@@ -71,14 +77,14 @@ func (f *Firewall) overrideWith(other Firewall) {
f.InputPorts = gosettings.OverrideWithSlice(f.InputPorts, other.InputPorts)
f.OutboundSubnets = gosettings.OverrideWithSlice(f.OutboundSubnets, other.OutboundSubnets)
f.Enabled = gosettings.OverrideWithPointer(f.Enabled, other.Enabled)
f.Debug = gosettings.OverrideWithPointer(f.Debug, other.Debug)
f.Implementation = gosettings.OverrideWithComparable(f.Implementation, other.Implementation)
f.Iptables.overrideWith(other.Iptables)
}
func (f *Firewall) setDefaults() {
func (f *Firewall) setDefaults(globalLogLevel string) {
f.Enabled = gosettings.DefaultPointer(f.Enabled, true)
f.Debug = gosettings.DefaultPointer(f.Debug, false)
f.Implementation = gosettings.DefaultComparable(f.Implementation, "iptables")
f.Iptables.setDefaults(globalLogLevel)
}
func (f Firewall) String() string {
@@ -94,10 +100,7 @@ func (f Firewall) toLinesNode() (node *gotree.Node) {
}
node.Appendf("Implementation: %s", f.Implementation)
if *f.Debug {
node.Appendf("Debug mode: on")
}
node.AppendNode(f.Iptables.toLinesNode())
if len(f.VPNInputPorts) > 0 {
vpnInputPortsNode := node.Appendf("VPN input ports:")
@@ -145,9 +148,9 @@ func (f *Firewall) read(r *reader.Reader) (err error) {
return err
}
f.Debug, err = r.BoolPtr("FIREWALL_DEBUG")
err = f.Iptables.read(r)
if err != nil {
return err
return fmt.Errorf("reading iptables settings: %w", err)
}
f.Implementation = r.String("FIREWALL_IMPLEMENTATION")
@@ -4,6 +4,7 @@ import (
"net/netip"
"testing"
"github.com/qdm12/log"
"github.com/stretchr/testify/assert"
)
@@ -12,22 +13,21 @@ func Test_Firewall_validate(t *testing.T) {
testCases := map[string]struct {
firewall Firewall
errWrapped error
errMessage string
}{
"empty": {},
"empty": {
errMessage: "iptables settings: log level: level is not recognized: ",
},
"zero_vpn_input_port": {
firewall: Firewall{
VPNInputPorts: []uint16{0},
},
errWrapped: ErrFirewallZeroPort,
errMessage: "VPN input ports: cannot have a zero port",
},
"zero_input_port": {
firewall: Firewall{
InputPorts: []uint16{0},
},
errWrapped: ErrFirewallZeroPort,
errMessage: "input ports: cannot have a zero port",
},
"unspecified_outbound_subnet": {
@@ -36,11 +36,11 @@ func Test_Firewall_validate(t *testing.T) {
netip.MustParsePrefix("0.0.0.0/0"),
},
},
errWrapped: ErrFirewallPublicOutboundSubnet,
errMessage: "outbound subnet has an unspecified address: 0.0.0.0/0",
},
"public_outbound_subnet": {
firewall: Firewall{
Iptables: Iptables{LogLevel: log.LevelInfo.String()},
OutboundSubnets: []netip.Prefix{
netip.MustParsePrefix("1.2.3.4/32"),
},
@@ -48,6 +48,7 @@ func Test_Firewall_validate(t *testing.T) {
},
"valid_settings": {
firewall: Firewall{
Iptables: Iptables{LogLevel: log.LevelInfo.String()},
VPNInputPorts: []uint16{100, 101},
InputPorts: []uint16{200, 201},
OutboundSubnets: []netip.Prefix{
@@ -64,9 +65,10 @@ func Test_Firewall_validate(t *testing.T) {
err := testCase.firewall.validate()
assert.ErrorIs(t, err, testCase.errWrapped)
if testCase.errWrapped != nil {
if testCase.errMessage != "" {
assert.EqualError(t, err, testCase.errMessage)
} else {
assert.NoError(t, err)
}
})
}
+4 -10
View File
@@ -38,12 +38,6 @@ type Health struct {
RestartVPN *bool
}
var (
ErrICMPTargetIPNotValid = errors.New("ICMP target IP address is not valid")
ErrICMPTargetIPsNotCompatible = errors.New("ICMP target IP addresses are not compatible")
ErrSmallCheckTypeNotValid = errors.New("small check type is not valid")
)
func (h Health) Validate() (err error) {
err = validate.ListeningAddress(h.ServerAddress, os.Getuid())
if err != nil {
@@ -53,16 +47,16 @@ func (h Health) Validate() (err error) {
for _, ip := range h.ICMPTargetIPs {
switch {
case !ip.IsValid():
return fmt.Errorf("%w: %s", ErrICMPTargetIPNotValid, ip)
return fmt.Errorf("ICMP target IP address is not valid: %s", ip)
case ip.IsUnspecified() && len(h.ICMPTargetIPs) > 1:
return fmt.Errorf("%w: only a single IP address must be set if it is to be unspecified",
ErrICMPTargetIPsNotCompatible)
return errors.New("ICMP target IP addresses are not compatible: " +
"only a single IP address must be set if it is to be unspecified")
}
}
err = validate.IsOneOf(h.SmallCheckType, "icmp", "dns")
if err != nil {
return fmt.Errorf("%w: %s", ErrSmallCheckTypeNotValid, err)
return fmt.Errorf("small check type is not valid: %w", err)
}
return nil
@@ -1,6 +1,6 @@
package settings
import gomock "github.com/golang/mock/gomock"
import gomock "go.uber.org/mock/gomock"
type sourceKeyValue struct {
key string
+2 -3
View File
@@ -48,7 +48,7 @@ func (h HTTPProxy) validate() (err error) {
// Do not validate user and password
err = validate.ListeningAddress(h.ListeningAddress, os.Getuid())
if err != nil {
return fmt.Errorf("%w: %s", ErrServerAddressNotValid, h.ListeningAddress)
return fmt.Errorf("server listening address is not valid: %w", err)
}
return nil
@@ -176,7 +176,6 @@ func readHTTProxyLog(r *reader.Reader) (enabled *bool, err error) {
case "disabled", "no", "off":
return ptrTo(false), nil
default:
return nil, fmt.Errorf("HTTP retro-compatible proxy log setting: %w: %s",
ErrValueUnknown, value)
return nil, fmt.Errorf("HTTP retro-compatible proxy log setting: value is unknown: %s", value)
}
}
@@ -0,0 +1,67 @@
package settings
import (
"fmt"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
"github.com/qdm12/log"
)
// Iptables contains settings to customize iptables.
type Iptables struct {
LogLevel string
}
func (i Iptables) validate() (err error) {
_, err = log.ParseLevel(i.LogLevel)
if err != nil {
return fmt.Errorf("log level: %w", err)
}
return nil
}
func (i *Iptables) copy() (copied Iptables) {
return Iptables{
LogLevel: i.LogLevel,
}
}
func (i *Iptables) overrideWith(other Iptables) {
i.LogLevel = gosettings.OverrideWithComparable(i.LogLevel, other.LogLevel)
}
func (i *Iptables) setDefaults(globalLogLevel string) {
defaultLevel := globalLogLevel
if defaultLevel == log.LevelDebug.String() {
// Given iptables debug logger is quite verbose, we only turn it to debug level
// if it is explicitly asked to be at debug level; even if the global logger is
// at the debug level, we keep iptables at info level by default.
defaultLevel = log.LevelInfo.String()
}
i.LogLevel = gosettings.DefaultComparable(i.LogLevel, defaultLevel)
}
func (i Iptables) String() string {
return i.toLinesNode().String()
}
func (i Iptables) toLinesNode() (node *gotree.Node) {
node = gotree.New("Iptables settings:")
node.Appendf("Log level: %s", i.LogLevel)
return node
}
func (i *Iptables) read(r *reader.Reader) (err error) {
debugMode, err := r.BoolPtr("FIREWALL_DEBUG", reader.IsRetro("FIREWALL_IPTABLES_LOG_LEVEL"))
if err != nil {
return err
}
if debugMode != nil && *debugMode {
i.LogLevel = log.LevelDebug.String()
}
i.LogLevel = r.String("FIREWALL_IPTABLES_LOG_LEVEL")
return nil
}
+58
View File
@@ -0,0 +1,58 @@
package settings
import (
"net/netip"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
)
// IPv6 contains settings regarding IPv6 configuration.
type IPv6 struct {
// CheckAddresses are the TCP ip:port addresses to dial to check if
// IPv6 is supported, in case a default IPv6 route is found.
// It defaults to google and cloudflare IPv6 anycast addresses
// [2001:4860:4860::8888]:53,[2606:4700:4700::1111]:53
CheckAddresses []netip.AddrPort
}
func (i IPv6) validate() (err error) {
return nil
}
func (i *IPv6) copy() (copied IPv6) {
return IPv6{
CheckAddresses: gosettings.CopySlice(i.CheckAddresses),
}
}
func (i *IPv6) overrideWith(other IPv6) {
i.CheckAddresses = gosettings.OverrideWithSlice(i.CheckAddresses, other.CheckAddresses)
}
func (i *IPv6) setDefaults() {
defaultCheckAddresses := []netip.AddrPort{
netip.MustParseAddrPort("[2001:4860:4860::8888]:53"),
netip.MustParseAddrPort("[2606:4700:4700::1111]:53"),
}
i.CheckAddresses = gosettings.DefaultSlice(i.CheckAddresses, defaultCheckAddresses)
}
func (i IPv6) String() string {
return i.toLinesNode().String()
}
func (i IPv6) toLinesNode() (node *gotree.Node) {
node = gotree.New("IPv6 settings:")
addrsNode := node.Appendf("Check addresses:")
for _, addr := range i.CheckAddresses {
addrsNode.Append(addr.String())
}
return node
}
func (i *IPv6) read(r *reader.Reader) (err error) {
i.CheckAddresses, err = r.CSVNetipAddrPorts("IPV6_CHECK_ADDRESSES")
return err
}
@@ -1,5 +1,10 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/qdm12/gosettings/reader (interfaces: Source)
//
// Generated by this command:
//
// mockgen -destination=mocks_reader_test.go -package=settings github.com/qdm12/gosettings/reader Source
//
// Package settings is a generated GoMock package.
package settings
@@ -7,13 +12,14 @@ package settings
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
gomock "go.uber.org/mock/gomock"
)
// MockSource is a mock of Source interface.
type MockSource struct {
ctrl *gomock.Controller
recorder *MockSourceMockRecorder
isgomock struct{}
}
// MockSourceMockRecorder is the mock recorder for MockSource.
@@ -34,32 +40,32 @@ func (m *MockSource) EXPECT() *MockSourceMockRecorder {
}
// Get mocks base method.
func (m *MockSource) Get(arg0 string) (string, bool) {
func (m *MockSource) Get(key string) (string, bool) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", arg0)
ret := m.ctrl.Call(m, "Get", key)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(bool)
return ret0, ret1
}
// Get indicates an expected call of Get.
func (mr *MockSourceMockRecorder) Get(arg0 interface{}) *gomock.Call {
func (mr *MockSourceMockRecorder) Get(key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockSource)(nil).Get), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockSource)(nil).Get), key)
}
// KeyTransform mocks base method.
func (m *MockSource) KeyTransform(arg0 string) string {
func (m *MockSource) KeyTransform(key string) string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "KeyTransform", arg0)
ret := m.ctrl.Call(m, "KeyTransform", key)
ret0, _ := ret[0].(string)
return ret0
}
// KeyTransform indicates an expected call of KeyTransform.
func (mr *MockSourceMockRecorder) KeyTransform(arg0 interface{}) *gomock.Call {
func (mr *MockSourceMockRecorder) KeyTransform(key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeyTransform", reflect.TypeOf((*MockSource)(nil).KeyTransform), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeyTransform", reflect.TypeOf((*MockSource)(nil).KeyTransform), key)
}
// String mocks base method.
+11 -5
View File
@@ -1,5 +1,10 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/qdm12/gluetun/internal/configuration/settings (interfaces: Warner)
//
// Generated by this command:
//
// mockgen -destination=mocks_test.go -package=settings . Warner
//
// Package settings is a generated GoMock package.
package settings
@@ -7,13 +12,14 @@ package settings
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
gomock "go.uber.org/mock/gomock"
)
// MockWarner is a mock of Warner interface.
type MockWarner struct {
ctrl *gomock.Controller
recorder *MockWarnerMockRecorder
isgomock struct{}
}
// MockWarnerMockRecorder is the mock recorder for MockWarner.
@@ -34,13 +40,13 @@ func (m *MockWarner) EXPECT() *MockWarnerMockRecorder {
}
// Warn mocks base method.
func (m *MockWarner) Warn(arg0 string) {
func (m *MockWarner) Warn(message string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Warn", arg0)
m.ctrl.Call(m, "Warn", message)
}
// Warn indicates an expected call of Warn.
func (mr *MockWarnerMockRecorder) Warn(arg0 interface{}) *gomock.Call {
func (mr *MockWarnerMockRecorder) Warn(message any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockWarner)(nil).Warn), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockWarner)(nil).Warn), message)
}
+12 -14
View File
@@ -2,6 +2,7 @@ package settings
import (
"encoding/base64"
"errors"
"fmt"
"regexp"
"strings"
@@ -92,7 +93,7 @@ func (o OpenVPN) validate(vpnProvider string) (err error) {
// Validate version
validVersions := []string{openvpn.Openvpn25, openvpn.Openvpn26}
if err = validate.IsOneOf(o.Version, validVersions...); err != nil {
return fmt.Errorf("%w: %w", ErrOpenVPNVersionIsNotValid, err)
return fmt.Errorf("version is not valid: %w", err)
}
isCustom := vpnProvider == providers.Custom
@@ -101,14 +102,14 @@ func (o OpenVPN) validate(vpnProvider string) (err error) {
vpnProvider != providers.VPNSecure
if isUserRequired && *o.User == "" {
return fmt.Errorf("%w", ErrOpenVPNUserIsEmpty)
return errors.New("user is empty")
}
passwordRequired := isUserRequired &&
(vpnProvider != providers.Ivpn || !ivpnAccountID.MatchString(*o.User))
if passwordRequired && *o.Password == "" {
return fmt.Errorf("%w", ErrOpenVPNPasswordIsEmpty)
return errors.New("password is empty")
}
err = validateOpenVPNConfigFilepath(isCustom, *o.ConfFile)
@@ -132,23 +133,20 @@ func (o OpenVPN) validate(vpnProvider string) (err error) {
}
if *o.EncryptedKey != "" && *o.KeyPassphrase == "" {
return fmt.Errorf("%w", ErrOpenVPNKeyPassphraseIsEmpty)
return errors.New("key passphrase is empty")
}
const maxMSSFix = 10000
if *o.MSSFix > maxMSSFix {
return fmt.Errorf("%w: %d is over the maximum value of %d",
ErrOpenVPNMSSFixIsTooHigh, *o.MSSFix, maxMSSFix)
return fmt.Errorf("mssfix option value is too high: %d is over the maximum value of %d", *o.MSSFix, maxMSSFix)
}
if !regexpInterfaceName.MatchString(o.Interface) {
return fmt.Errorf("%w: '%s' does not match regex '%s'",
ErrOpenVPNInterfaceNotValid, o.Interface, regexpInterfaceName)
return fmt.Errorf("interface name is not valid: '%s' does not match regex '%s'", o.Interface, regexpInterfaceName)
}
if *o.Verbosity < 0 || *o.Verbosity > 6 {
return fmt.Errorf("%w: %d can only be between 0 and 5",
ErrOpenVPNVerbosityIsOutOfBounds, o.Verbosity)
return fmt.Errorf("verbosity value is out of bounds: %d can only be between 0 and 5", o.Verbosity)
}
return nil
@@ -162,7 +160,7 @@ func validateOpenVPNConfigFilepath(isCustom bool,
}
if confFile == "" {
return fmt.Errorf("%w", ErrFilepathMissing)
return errors.New("filepath is missing")
}
err = validate.FileExists(confFile)
@@ -189,7 +187,7 @@ func validateOpenVPNClientCertificate(vpnProvider,
providers.VPNSecure,
providers.VPNUnlimited:
if clientCert == "" {
return fmt.Errorf("%w", ErrMissingValue)
return errors.New("missing value")
}
}
@@ -211,7 +209,7 @@ func validateOpenVPNClientKey(vpnProvider, clientKey string) (err error) {
providers.Cyberghost,
providers.VPNUnlimited:
if clientKey == "" {
return fmt.Errorf("%w", ErrMissingValue)
return errors.New("missing value")
}
}
@@ -230,7 +228,7 @@ func validateOpenVPNEncryptedKey(vpnProvider,
encryptedPrivateKey string,
) (err error) {
if vpnProvider == providers.VPNSecure && encryptedPrivateKey == "" {
return fmt.Errorf("%w", ErrMissingValue)
return errors.New("missing value")
}
if encryptedPrivateKey == "" {
@@ -59,12 +59,9 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
if o.Protocol == constants.TCP && helpers.IsOneOf(vpnProvider,
providers.Giganews,
providers.Ipvanish,
providers.Perfectprivacy,
providers.Privado,
providers.Vyprvpn,
) {
return fmt.Errorf("%w: for VPN service provider %s",
ErrOpenVPNTCPNotSupported, vpnProvider)
return fmt.Errorf("TCP protocol is not supported: for VPN service provider %s", vpnProvider)
}
// Validate CustomPort
@@ -75,12 +72,11 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
providers.Privatevpn, providers.Torguard:
// no custom port allowed
case providers.Expressvpn, providers.Fastestvpn,
providers.Giganews, providers.Ipvanish, providers.Nordvpn,
providers.Privado, providers.Purevpn,
providers.Giganews, providers.Ipvanish,
providers.Nordvpn, providers.Purevpn,
providers.Surfshark, providers.VPNSecure,
providers.VPNUnlimited, providers.Vyprvpn:
return fmt.Errorf("%w: for VPN service provider %s",
ErrOpenVPNCustomPortNotAllowed, vpnProvider)
return fmt.Errorf("custom endpoint port is not allowed: for VPN service provider %s", vpnProvider)
default:
var allowedTCP, allowedUDP []uint16
switch vpnProvider {
@@ -96,11 +92,11 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
case providers.Mullvad:
allowedTCP = []uint16{80, 443, 1401}
allowedUDP = []uint16{53, 1194, 1195, 1196, 1197, 1300, 1301, 1302, 1303, 1400}
case providers.Perfectprivacy:
allowedTCP = []uint16{44, 443, 4433}
allowedUDP = []uint16{44, 443, 4433}
case providers.Privado:
allowedTCP = []uint16{443, 1194, 8080, 8443}
allowedUDP = []uint16{443, 1194, 8080, 8443}
case providers.PrivateInternetAccess:
allowedTCP = []uint16{80, 110, 443}
allowedTCP = []uint16{80, 110, 443, 501, 502, 8443}
allowedUDP = []uint16{53, 1194, 1197, 1198, 8080, 9201}
case providers.Protonvpn:
allowedTCP = []uint16{443, 5995, 8443}
@@ -121,8 +117,7 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
}
err = validate.IsOneOf(*o.CustomPort, allowedPorts...)
if err != nil {
return fmt.Errorf("%w: for VPN service provider %s: %w",
ErrOpenVPNCustomPortNotAllowed, vpnProvider, err)
return fmt.Errorf("custom endpoint port is not allowed: for VPN service provider %s: %w", vpnProvider, err)
}
}
}
@@ -130,12 +125,11 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
// Validate EncPreset
if vpnProvider == providers.PrivateInternetAccess {
validEncryptionPresets := []string{
presets.None,
presets.Normal,
presets.Strong,
}
if err = validate.IsOneOf(*o.PIAEncPreset, validEncryptionPresets...); err != nil {
return fmt.Errorf("%w: %w", ErrOpenVPNEncryptionPresetNotValid, err)
return fmt.Errorf("PIA encryption preset is not valid: %w", err)
}
}
+2 -8
View File
@@ -1,7 +1,6 @@
package settings
import (
"errors"
"fmt"
"net/netip"
@@ -24,21 +23,16 @@ type PMTUD struct {
TCPAddresses []netip.AddrPort `json:"tcp_addresses"`
}
var (
ErrPMTUDICMPAddressNotValid = errors.New("PMTUD ICMP address is not valid")
ErrPMTUDTCPAddressNotValid = errors.New("PMTUD TCP address is not valid")
)
// Validate validates PMTUD settings.
func (p PMTUD) validate() (err error) {
for i, addr := range p.ICMPAddresses {
if !addr.IsValid() {
return fmt.Errorf("%w: at index %d", ErrPMTUDICMPAddressNotValid, i)
return fmt.Errorf("PMTUD ICMP address is not valid: at index %d", i)
}
}
for i, addr := range p.TCPAddresses {
if !addr.IsValid() {
return fmt.Errorf("%w: at index %d", ErrPMTUDTCPAddressNotValid, i)
return fmt.Errorf("PMTUD TCP address is not valid: at index %d", i)
}
}
return nil
+65 -24
View File
@@ -1,8 +1,10 @@
package settings
import (
"errors"
"fmt"
"path/filepath"
"slices"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gosettings"
@@ -37,10 +39,16 @@ type PortForwarding struct {
// It can be the empty string to indicate to NOT run a command.
// It cannot be nil in the internal state.
DownCommand *string `json:"down_command"`
// ListeningPort is the port traffic would be redirected to from the
// forwarded port. The redirection is disabled if it is set to 0, which
// is its default as well.
ListeningPort *uint16 `json:"listening_port"`
// ListeningPorts are the ports traffic would be redirected to from the
// forwarded ports. The redirection is disabled if it is the slice [0],
// which is its default as well. If set and not [0], its length must match
// the PortsCount value, such that each forwarded port is redirected to
// the corresponding listening port.
ListeningPorts []uint16 `json:"listening_port"`
// PortsCount is the number of ports to forward. It is optional for ProtonVPN
// and be between 1 and 5. For other providers, it must be set to 1 if port
// forwarding is enabled.
PortsCount uint16 `json:"ports_count"`
// Username is only used for Private Internet Access port forwarding.
Username string `json:"username"`
// Password is only used for Private Internet Access port forwarding.
@@ -58,13 +66,12 @@ func (p PortForwarding) Validate(vpnProvider string) (err error) {
providerSelected = *p.Provider
}
validProviders := []string{
providers.Perfectprivacy,
providers.PrivateInternetAccess,
providers.Privatevpn,
providers.Protonvpn,
}
if err = validate.IsOneOf(providerSelected, validProviders...); err != nil {
return fmt.Errorf("%w: %w", ErrPortForwardingEnabled, err)
return fmt.Errorf("port forwarding cannot be enabled: %w", err)
}
// Validate Filepath
@@ -75,12 +82,36 @@ func (p PortForwarding) Validate(vpnProvider string) (err error) {
}
}
if providerSelected == providers.PrivateInternetAccess {
switch providerSelected {
case providers.PrivateInternetAccess:
const maxPortsCount = 1
switch {
case p.PortsCount > maxPortsCount:
return fmt.Errorf("ports count too high: %d > %d", p.PortsCount, maxPortsCount)
case p.Username == "":
return fmt.Errorf("%w", ErrPortForwardingUserEmpty)
return errors.New("port forwarding username is empty")
case p.Password == "":
return fmt.Errorf("%w", ErrPortForwardingPasswordEmpty)
return errors.New("port forwarding password is empty")
}
case providers.Protonvpn:
const maxPortsCount = 5
if p.PortsCount > maxPortsCount {
return fmt.Errorf("ports count too high: %d > %d", p.PortsCount, maxPortsCount)
}
default:
const maxPortsCount = 1
if p.PortsCount > maxPortsCount {
return fmt.Errorf("ports count too high: %d > %d", p.PortsCount, maxPortsCount)
}
}
if !slices.Equal(p.ListeningPorts, []uint16{0}) {
switch {
case len(p.ListeningPorts) != int(p.PortsCount):
return fmt.Errorf("listening ports length must be equal to ports count: "+
"%d != %d", len(p.ListeningPorts), p.PortsCount)
case slices.Contains(p.ListeningPorts, 0):
return fmt.Errorf("listening port cannot be 0: in %v", p.ListeningPorts)
}
}
@@ -89,14 +120,14 @@ func (p PortForwarding) Validate(vpnProvider string) (err error) {
func (p *PortForwarding) Copy() (copied PortForwarding) {
return PortForwarding{
Enabled: gosettings.CopyPointer(p.Enabled),
Provider: gosettings.CopyPointer(p.Provider),
Filepath: gosettings.CopyPointer(p.Filepath),
UpCommand: gosettings.CopyPointer(p.UpCommand),
DownCommand: gosettings.CopyPointer(p.DownCommand),
ListeningPort: gosettings.CopyPointer(p.ListeningPort),
Username: p.Username,
Password: p.Password,
Enabled: gosettings.CopyPointer(p.Enabled),
Provider: gosettings.CopyPointer(p.Provider),
Filepath: gosettings.CopyPointer(p.Filepath),
UpCommand: gosettings.CopyPointer(p.UpCommand),
DownCommand: gosettings.CopyPointer(p.DownCommand),
ListeningPorts: gosettings.CopySlice(p.ListeningPorts),
Username: p.Username,
Password: p.Password,
}
}
@@ -106,7 +137,7 @@ func (p *PortForwarding) OverrideWith(other PortForwarding) {
p.Filepath = gosettings.OverrideWithPointer(p.Filepath, other.Filepath)
p.UpCommand = gosettings.OverrideWithPointer(p.UpCommand, other.UpCommand)
p.DownCommand = gosettings.OverrideWithPointer(p.DownCommand, other.DownCommand)
p.ListeningPort = gosettings.OverrideWithPointer(p.ListeningPort, other.ListeningPort)
p.ListeningPorts = gosettings.OverrideWithSlice(p.ListeningPorts, other.ListeningPorts)
p.Username = gosettings.OverrideWithComparable(p.Username, other.Username)
p.Password = gosettings.OverrideWithComparable(p.Password, other.Password)
}
@@ -117,7 +148,8 @@ func (p *PortForwarding) setDefaults() {
p.Filepath = gosettings.DefaultPointer(p.Filepath, "/tmp/gluetun/forwarded_port")
p.UpCommand = gosettings.DefaultPointer(p.UpCommand, "")
p.DownCommand = gosettings.DefaultPointer(p.DownCommand, "")
p.ListeningPort = gosettings.DefaultPointer(p.ListeningPort, 0)
p.ListeningPorts = gosettings.DefaultSlice(p.ListeningPorts, []uint16{0}) // disabled
p.PortsCount = gosettings.DefaultComparable(p.PortsCount, 1)
}
func (p PortForwarding) String() string {
@@ -131,11 +163,14 @@ func (p PortForwarding) toLinesNode() (node *gotree.Node) {
node = gotree.New("Automatic port forwarding settings:")
listeningPort := "disabled"
if *p.ListeningPort != 0 {
listeningPort = fmt.Sprintf("%d", *p.ListeningPort)
node.Appendf("Number of ports to be forwarded: %d", p.PortsCount)
if !slices.Equal(p.ListeningPorts, []uint16{0}) {
redirNode := node.Appendf("Redirection for listening ports:")
for i, port := range p.ListeningPorts {
redirNode.Appendf("Port #%d -> %d", i+1, port)
}
}
node.Appendf("Redirection listening port: %s", listeningPort)
if *p.Provider == "" {
node.Appendf("Use port forwarding code for current provider")
@@ -190,7 +225,13 @@ func (p *PortForwarding) read(r *reader.Reader) (err error) {
p.DownCommand = r.Get("VPN_PORT_FORWARDING_DOWN_COMMAND",
reader.ForceLowercase(false))
p.ListeningPort, err = r.Uint16Ptr("VPN_PORT_FORWARDING_LISTENING_PORT")
p.ListeningPorts, err = r.CSVUint16("VPN_PORT_FORWARDING_LISTENING_PORTS",
reader.RetroKeys("VPN_PORT_FORWARDING_LISTENING_PORT"))
if err != nil {
return err
}
p.PortsCount, err = r.Uint16("VPN_PORT_FORWARDING_PORTS_COUNT")
if err != nil {
return err
}
+6 -3
View File
@@ -30,7 +30,10 @@ type Provider struct {
func (p *Provider) validate(vpnType string, filterChoicesGetter FilterChoicesGetter, warner Warner) (err error) {
// Validate Name
var validNames []string
if vpnType == vpn.OpenVPN {
switch vpnType {
case vpn.AmneziaWg:
validNames = []string{providers.Custom}
case vpn.OpenVPN:
validNames = providers.AllWithCustom()
validNames = append(validNames, "pia") // Retro-compatibility
// Remove Mullvad since it no longer supports OpenVPN as of January 15th, 2026
@@ -38,7 +41,7 @@ func (p *Provider) validate(vpnType string, filterChoicesGetter FilterChoicesGet
validNames[mullvadIndex], validNames[len(validNames)-1] = validNames[len(validNames)-1], validNames[mullvadIndex]
validNames = validNames[:len(validNames)-1]
sort.Strings(validNames)
} else { // Wireguard
case vpn.Wireguard:
validNames = []string{
providers.Airvpn,
providers.Custom,
@@ -52,7 +55,7 @@ func (p *Provider) validate(vpnType string, filterChoicesGetter FilterChoicesGet
}
}
if err = validate.IsOneOf(p.Name, validNames...); err != nil {
return fmt.Errorf("%w for Wireguard: %w", ErrVPNProviderNameNotValid, err)
return fmt.Errorf("VPN provider name is not valid for %s: %w", vpnType, err)
}
err = p.ServerSelection.validate(p.Name, filterChoicesGetter, warner)
@@ -3,9 +3,9 @@ package settings
import (
"testing"
"github.com/golang/mock/gomock"
"github.com/qdm12/gosettings/reader"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
)
func Test_PublicIP_read(t *testing.T) {
@@ -15,7 +15,6 @@ func Test_PublicIP_read(t *testing.T) {
makeReader func(ctrl *gomock.Controller) *reader.Reader
makeWarner func(ctrl *gomock.Controller) Warner
settings PublicIP
errWrapped error
errMessage string
}{
"nothing_read": {
@@ -152,9 +151,10 @@ func Test_PublicIP_read(t *testing.T) {
err := settings.read(reader, warner)
assert.Equal(t, testCase.settings, settings)
assert.ErrorIs(t, err, testCase.errWrapped)
if testCase.errWrapped != nil {
if testCase.errMessage != "" {
assert.EqualError(t, err, testCase.errMessage)
} else {
assert.NoError(t, err)
}
})
}
+2 -3
View File
@@ -46,8 +46,7 @@ func (c ControlServer) validate() (err error) {
uid := os.Getuid()
const maxPrivilegedPort = 1023
if uid != 0 && port != 0 && port <= maxPrivilegedPort {
return fmt.Errorf("%w: %d when running with user ID %d",
ErrControlServerPrivilegedPort, port, uid)
return fmt.Errorf("cannot use privileged port without running as root: %d when running with user ID %d", port, uid)
}
jsonDecoder := json.NewDecoder(bytes.NewBufferString(c.AuthDefaultRole))
@@ -96,7 +95,7 @@ func (c *ControlServer) setDefaults() {
var role auth.Role
_ = json.Unmarshal([]byte(c.AuthDefaultRole), &role)
role.Name = "default"
roleBytes, _ := json.Marshal(role) //nolint:errchkjson
roleBytes, _ := json.Marshal(role) //nolint:errchkjson,gosec
c.AuthDefaultRole = string(roleBytes)
}
}
@@ -71,25 +71,13 @@ type ServerSelection struct {
Wireguard WireguardSelection `json:"wireguard"`
}
var (
ErrOwnedOnlyNotSupported = errors.New("owned only filter is not supported")
ErrFreeOnlyNotSupported = errors.New("free only filter is not supported")
ErrPremiumOnlyNotSupported = errors.New("premium only filter is not supported")
ErrStreamOnlyNotSupported = errors.New("stream only filter is not supported")
ErrMultiHopOnlyNotSupported = errors.New("multi hop only filter is not supported")
ErrPortForwardOnlyNotSupported = errors.New("port forwarding only filter is not supported")
ErrFreePremiumBothSet = errors.New("free only and premium only filters are both set")
ErrSecureCoreOnlyNotSupported = errors.New("secure core only filter is not supported")
ErrTorOnlyNotSupported = errors.New("tor only filter is not supported")
)
func (ss *ServerSelection) validate(vpnServiceProvider string,
filterChoicesGetter FilterChoicesGetter, warner Warner,
) (err error) {
switch ss.VPN {
case vpn.OpenVPN, vpn.Wireguard:
case vpn.AmneziaWg, vpn.OpenVPN, vpn.Wireguard:
default:
return fmt.Errorf("%w: %s", ErrVPNTypeNotValid, ss.VPN)
return fmt.Errorf("VPN type is not valid: %s", ss.VPN)
}
filterChoices, err := getLocationFilterChoices(vpnServiceProvider, ss, filterChoicesGetter, warner)
@@ -150,7 +138,7 @@ func getLocationFilterChoices(vpnServiceProvider string,
// Only return error comparing with newer regions, we don't want to confuse the user
// with the retro regions in the error message.
err = atLeastOneIsOneOfCaseInsensitive(ss.Regions, filterChoices.Regions, warner)
return models.FilterChoices{}, fmt.Errorf("%w: %w", ErrRegionNotValid, err)
return models.FilterChoices{}, fmt.Errorf("the region specified is not valid: %w", err)
}
}
@@ -164,27 +152,27 @@ func validateServerFilters(settings ServerSelection, filterChoices models.Filter
) (err error) {
err = atLeastOneIsOneOfCaseInsensitive(settings.Countries, filterChoices.Countries, warner)
if err != nil {
return fmt.Errorf("%w: %w", ErrCountryNotValid, err)
return fmt.Errorf("the country specified is not valid: %w", err)
}
err = atLeastOneIsOneOfCaseInsensitive(settings.Regions, filterChoices.Regions, warner)
if err != nil {
return fmt.Errorf("%w: %w", ErrRegionNotValid, err)
return fmt.Errorf("the region specified is not valid: %w", err)
}
err = atLeastOneIsOneOfCaseInsensitive(settings.Cities, filterChoices.Cities, warner)
if err != nil {
return fmt.Errorf("%w: %w", ErrCityNotValid, err)
return fmt.Errorf("the city specified is not valid: %w", err)
}
err = atLeastOneIsOneOfCaseInsensitive(settings.ISPs, filterChoices.ISPs, warner)
if err != nil {
return fmt.Errorf("%w: %w", ErrISPNotValid, err)
return fmt.Errorf("the ISP specified is not valid: %w", err)
}
err = atLeastOneIsOneOfCaseInsensitive(settings.Hostnames, filterChoices.Hostnames, warner)
if err != nil {
return fmt.Errorf("%w: %w", ErrHostnameNotValid, err)
return fmt.Errorf("the hostname specified is not valid: %w", err)
}
if vpnServiceProvider == providers.Custom {
@@ -196,19 +184,19 @@ func validateServerFilters(settings ServerSelection, filterChoices models.Filter
// which requires a server name for TLS verification.
filterChoices.Names = settings.Names
default:
return fmt.Errorf("%w: %d names specified instead of "+
"0 or 1 for the custom provider",
ErrNameNotValid, len(settings.Names))
return fmt.Errorf("name is not valid: "+
"%d names specified instead of 0 or 1 for the custom provider",
len(settings.Names))
}
}
err = atLeastOneIsOneOfCaseInsensitive(settings.Names, filterChoices.Names, warner)
if err != nil {
return fmt.Errorf("%w: %w", ErrNameNotValid, err)
return fmt.Errorf("the server name specified is not valid: %w", err)
}
err = atLeastOneIsOneOfCaseInsensitive(settings.Categories, filterChoices.Categories, warner)
if err != nil {
return fmt.Errorf("%w: %w", ErrCategoryNotValid, err)
return fmt.Errorf("the category specified is not valid: %w", err)
}
return nil
@@ -255,12 +243,12 @@ func validateSubscriptionTierFilters(settings ServerSelection, vpnServiceProvide
switch {
case *settings.FreeOnly &&
!helpers.IsOneOf(vpnServiceProvider, providers.Protonvpn, providers.VPNUnlimited):
return fmt.Errorf("%w", ErrFreeOnlyNotSupported)
return errors.New("free only filter is not supported")
case *settings.PremiumOnly &&
!helpers.IsOneOf(vpnServiceProvider, providers.VPNSecure):
return fmt.Errorf("%w", ErrPremiumOnlyNotSupported)
return errors.New("premium only filter is not supported")
case *settings.FreeOnly && *settings.PremiumOnly:
return fmt.Errorf("%w", ErrFreePremiumBothSet)
return errors.New("free only and premium only filters are both set")
default:
return nil
}
@@ -269,21 +257,21 @@ func validateSubscriptionTierFilters(settings ServerSelection, vpnServiceProvide
func validateFeatureFilters(settings ServerSelection, vpnServiceProvider string) error {
switch {
case *settings.OwnedOnly && vpnServiceProvider != providers.Mullvad:
return fmt.Errorf("%w", ErrOwnedOnlyNotSupported)
return errors.New("owned only filter is not supported")
case vpnServiceProvider == providers.Protonvpn && *settings.FreeOnly && *settings.PortForwardOnly:
return fmt.Errorf("%w: together with free only filter", ErrPortForwardOnlyNotSupported)
return errors.New("port forwarding only filter is not supported: together with free only filter")
case *settings.StreamOnly &&
!helpers.IsOneOf(vpnServiceProvider, providers.Protonvpn, providers.VPNUnlimited):
return fmt.Errorf("%w", ErrStreamOnlyNotSupported)
return errors.New("stream only filter is not supported")
case *settings.MultiHopOnly && vpnServiceProvider != providers.Surfshark:
return fmt.Errorf("%w", ErrMultiHopOnlyNotSupported)
return errors.New("multi hop only filter is not supported")
case *settings.PortForwardOnly &&
!helpers.IsOneOf(vpnServiceProvider, providers.PrivateInternetAccess, providers.Protonvpn):
return fmt.Errorf("%w", ErrPortForwardOnlyNotSupported)
return errors.New("port forwarding only filter is not supported")
case *settings.SecureCoreOnly && vpnServiceProvider != providers.Protonvpn:
return fmt.Errorf("%w", ErrSecureCoreOnlyNotSupported)
return errors.New("secure core only filter is not supported")
case *settings.TorOnly && vpnServiceProvider != providers.Protonvpn:
return fmt.Errorf("%w", ErrTorOnlyNotSupported)
return errors.New("tor only filter is not supported")
default:
return nil
}
@@ -518,7 +506,8 @@ func (ss *ServerSelection) read(r *reader.Reader,
return err
}
err = ss.Wireguard.read(r)
amneziawg := ss.VPN == vpn.AmneziaWg
err = ss.Wireguard.read(r, amneziawg)
if err != nil {
return err
}
+30 -11
View File
@@ -2,7 +2,6 @@ package settings
import (
"fmt"
"net/netip"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants/providers"
@@ -21,13 +20,16 @@ type Settings struct {
HTTPProxy HTTPProxy
Log Log
PublicIP PublicIP
Socks5 Socks5
Shadowsocks Shadowsocks
Storage Storage
System System
Updater Updater
Version Version
VPN VPN
IPv6 IPv6
Pprof pprof.Settings
BoringPoll BoringPoll
}
type FilterChoicesGetter interface {
@@ -48,15 +50,18 @@ func (s *Settings) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Support
"http proxy": s.HTTPProxy.validate,
"log": s.Log.validate,
"public ip check": s.PublicIP.validate,
"socks5": s.Socks5.validate,
"shadowsocks": s.Shadowsocks.validate,
"storage": s.Storage.validate,
"system": s.System.validate,
"updater": s.Updater.Validate,
"version": s.Version.validate,
"ipv6": s.IPv6.validate,
// Pprof validation done in pprof constructor
"VPN": func() error {
return s.VPN.Validate(filterChoicesGetter, ipv6Supported, warner)
},
"boring poll": s.BoringPoll.validate,
}
for name, validation := range nameToValidation {
@@ -78,6 +83,7 @@ func (s *Settings) copy() (copied Settings) {
HTTPProxy: s.HTTPProxy.copy(),
Log: s.Log.copy(),
PublicIP: s.PublicIP.copy(),
Socks5: s.Socks5.copy(),
Shadowsocks: s.Shadowsocks.copy(),
Storage: s.Storage.copy(),
System: s.System.copy(),
@@ -85,6 +91,8 @@ func (s *Settings) copy() (copied Settings) {
Version: s.Version.copy(),
VPN: s.VPN.Copy(),
Pprof: s.Pprof.Copy(),
BoringPoll: s.BoringPoll.Copy(),
IPv6: s.IPv6.copy(),
}
}
@@ -99,6 +107,7 @@ func (s *Settings) OverrideWith(other Settings,
patchedSettings.HTTPProxy.overrideWith(other.HTTPProxy)
patchedSettings.Log.overrideWith(other.Log)
patchedSettings.PublicIP.overrideWith(other.PublicIP)
patchedSettings.Socks5.overrideWith(other.Socks5)
patchedSettings.Shadowsocks.overrideWith(other.Shadowsocks)
patchedSettings.Storage.overrideWith(other.Storage)
patchedSettings.System.overrideWith(other.System)
@@ -106,6 +115,8 @@ func (s *Settings) OverrideWith(other Settings,
patchedSettings.Version.overrideWith(other.Version)
patchedSettings.VPN.OverrideWith(other.VPN)
patchedSettings.Pprof.OverrideWith(other.Pprof)
patchedSettings.BoringPoll.overrideWith(other.BoringPoll)
patchedSettings.IPv6.overrideWith(other.IPv6)
err = patchedSettings.Validate(filterChoicesGetter, ipv6Supported, warner)
if err != nil {
return err
@@ -117,18 +128,22 @@ func (s *Settings) OverrideWith(other Settings,
func (s *Settings) SetDefaults() {
s.ControlServer.setDefaults()
s.DNS.setDefaults()
s.Firewall.setDefaults()
s.Log.setDefaults()
s.Firewall.setDefaults(s.Log.Level)
s.Health.SetDefaults()
s.HTTPProxy.setDefaults()
s.Log.setDefaults()
s.IPv6.setDefaults()
s.PublicIP.setDefaults()
s.Socks5.setDefaults()
s.Shadowsocks.setDefaults()
s.Storage.setDefaults()
s.Storage.SetDefaults()
s.System.setDefaults()
s.Version.setDefaults()
s.VPN.setDefaults()
s.Updater.SetDefaults(s.VPN.Provider.Name)
s.Pprof.SetDefaults()
s.BoringPoll.setDefaults()
}
func (s Settings) String() string {
@@ -142,7 +157,9 @@ func (s Settings) toLinesNode() (node *gotree.Node) {
node.AppendNode(s.DNS.toLinesNode())
node.AppendNode(s.Firewall.toLinesNode())
node.AppendNode(s.Log.toLinesNode())
node.AppendNode(s.IPv6.toLinesNode())
node.AppendNode(s.Health.toLinesNode())
node.AppendNode(s.Socks5.toLinesNode())
node.AppendNode(s.Shadowsocks.toLinesNode())
node.AppendNode(s.HTTPProxy.toLinesNode())
node.AppendNode(s.ControlServer.toLinesNode())
@@ -152,6 +169,7 @@ func (s Settings) toLinesNode() (node *gotree.Node) {
node.AppendNode(s.Updater.toLinesNode())
node.AppendNode(s.Version.toLinesNode())
node.AppendNode(s.Pprof.ToLinesNode())
node.AppendNode(s.BoringPoll.toLinesNode())
return node
}
@@ -174,13 +192,11 @@ func (s Settings) Warnings() (warnings []string) {
"by creating an issue, attaching the new certificate and we will update Gluetun.")
}
// TODO remove in v4
if s.DNS.ServerAddress.Unmap().Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) != 0 {
warnings = append(warnings, "DNS address is set to "+s.DNS.ServerAddress.String()+
" so the local forwarding DNS server will not be used."+
" The default value changed to 127.0.0.1 so it uses the internal DNS server."+
" If this server fails to start, the IPv4 address of the first plaintext DNS server"+
" corresponding to the first DNS provider chosen is used.")
for _, upstreamAddress := range s.DNS.UpstreamPlainAddresses {
if upstreamAddress.Addr().IsPrivate() {
warnings = append(warnings, "DNS upstream address "+upstreamAddress.String()+" is private: "+
"DNS traffic might leak out of the VPN tunnel to that address.")
}
}
return warnings
@@ -202,13 +218,16 @@ func (s *Settings) Read(r *reader.Reader, warner Warner) (err error) {
"public ip": func(r *reader.Reader) error {
return s.PublicIP.read(r, warner)
},
"socks5": s.Socks5.read,
"shadowsocks": s.Shadowsocks.read,
"storage": s.Storage.read,
"storage": s.Storage.Read,
"system": s.System.read,
"updater": s.Updater.read,
"version": s.Version.read,
"VPN": s.VPN.read,
"IPv6": s.IPv6.read,
"profiling": s.Pprof.Read,
"boring poll": s.BoringPoll.read,
}
for name, read := range readFunctions {
@@ -51,9 +51,6 @@ func Test_Settings_String(t *testing.T) {
| [2606:4700:4700::1111]:443
| [2001:4860:4860::8888]:443
DNS settings:
| Keep existing nameserver(s): no
| DNS server address to use: 127.0.0.1
| DNS forwarder server enabled: yes
| Upstream resolver type: dot
| Upstream resolvers:
| | Cloudflare
@@ -62,13 +59,18 @@ func Test_Settings_String(t *testing.T) {
| Update period: every 24h0m0s
| DNS filtering settings:
| Block malicious: yes
| Block ads: no
| Block surveillance: yes
| Block ads: no
Firewall settings:
| Enabled: yes
| Implementation: iptables
| Implementation: iptables
| Iptables settings:
| Log level: INFO
Log settings:
| Log level: INFO
IPv6 settings:
| Check addresses:
| [2001:4860:4860::8888]:53
| [2606:4700:4700::1111]:53
Health settings:
| Server listening address: 127.0.0.1:9999
| Target addresses:
@@ -79,6 +81,8 @@ func Test_Settings_String(t *testing.T) {
| | 1.1.1.1
| | 8.8.8.8
| Restart VPN on healthcheck failure: yes
SOCKS5 proxy server settings:
| Enabled: no
Shadowsocks server settings:
| Enabled: no
HTTP proxy settings:
@@ -88,7 +92,7 @@ func Test_Settings_String(t *testing.T) {
| Logging: yes
| Authentication file path: /gluetun/auth/config.toml
Storage settings:
| Filepath: /gluetun/servers.json
| Servers directory path: /gluetun/servers/
OS Alpine settings:
| Process UID: 1000
| Process GID: 1000
+91
View File
@@ -0,0 +1,91 @@
package settings
import (
"errors"
"fmt"
"os"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree"
)
// Socks5 contains settings to configure the Socks5 proxy server.
type Socks5 struct {
Enabled *bool
ListeningAddress string
Username *string
Password *string
}
func (s Socks5) validate() (err error) {
err = validate.ListeningAddress(s.ListeningAddress, os.Getuid())
if err != nil {
return fmt.Errorf("server listening address is not valid: %w", err)
}
switch {
case *s.Username != "" && *s.Password == "":
return errors.New("password must be set if username is set")
case *s.Username == "" && *s.Password != "":
return errors.New("username must be set if password is set")
}
return nil
}
func (s *Socks5) copy() (copied Socks5) {
return Socks5{
Enabled: gosettings.CopyPointer(s.Enabled),
ListeningAddress: s.ListeningAddress,
Username: gosettings.CopyPointer(s.Username),
Password: gosettings.CopyPointer(s.Password),
}
}
func (s *Socks5) overrideWith(other Socks5) {
s.Enabled = gosettings.OverrideWithPointer(s.Enabled, other.Enabled)
s.ListeningAddress = gosettings.OverrideWithComparable(s.ListeningAddress, other.ListeningAddress)
s.Username = gosettings.OverrideWithPointer(s.Username, other.Username)
s.Password = gosettings.OverrideWithPointer(s.Password, other.Password)
}
func (s *Socks5) setDefaults() {
s.Enabled = gosettings.DefaultPointer(s.Enabled, false)
s.ListeningAddress = gosettings.DefaultComparable(s.ListeningAddress, ":1080")
s.Username = gosettings.DefaultPointer(s.Username, "")
s.Password = gosettings.DefaultPointer(s.Password, "")
}
func (s Socks5) String() string {
return s.toLinesNode().String()
}
func (s Socks5) toLinesNode() (node *gotree.Node) {
node = gotree.New("SOCKS5 proxy server settings:")
node.Appendf("Enabled: %s", gosettings.BoolToYesNo(s.Enabled))
if !*s.Enabled {
return node
}
node.Appendf("Listening address: %s", s.ListeningAddress)
if *s.Username != "" || *s.Password != "" {
node.Appendf("Username: %s", *s.Username)
node.Appendf("Password: %s", gosettings.ObfuscateKey(*s.Password))
}
return node
}
func (s *Socks5) read(r *reader.Reader) (err error) {
s.Enabled, err = r.BoolPtr("SOCKS5_ENABLED")
if err != nil {
return err
}
s.ListeningAddress = r.String("SOCKS5_LISTENING_ADDRESS")
s.Username = r.Get("SOCKS5_USER", reader.ForceLowercase(false))
s.Password = r.Get("SOCKS5_PASSWORD", reader.ForceLowercase(false))
return nil
}
+51 -14
View File
@@ -11,15 +11,26 @@ import (
// Storage contains settings to configure the storage.
type Storage struct {
// Filepath is the path to the servers.json file. An empty string disables on-disk storage.
Filepath *string
// ServersEnabled is whether to enable storage of servers on disk.
// It defaults to true.
ServersEnabled *bool
// ServersPath is the path to the servers files directory, and cannot be
// the empty string.
ServersPath string
// LegacyServersFilepath is the legacy "fat" JSON filepath to migrate from.
// TODO v4: remove
LegacyServersFilepath string
}
func (s Storage) validate() (err error) {
if *s.Filepath != "" { // optional
_, err := filepath.Abs(*s.Filepath)
if *s.ServersEnabled {
_, err := filepath.Abs(s.ServersPath)
if err != nil {
return fmt.Errorf("filepath is not valid: %w", err)
return fmt.Errorf("servers path is not valid: %w", err)
}
_, err = filepath.Abs(s.LegacyServersFilepath)
if err != nil {
return fmt.Errorf("legacy servers filepath is not valid: %w", err)
}
}
return nil
@@ -27,17 +38,25 @@ func (s Storage) validate() (err error) {
func (s *Storage) copy() (copied Storage) {
return Storage{
Filepath: gosettings.CopyPointer(s.Filepath),
ServersEnabled: gosettings.CopyPointer(s.ServersEnabled),
ServersPath: s.ServersPath,
LegacyServersFilepath: s.LegacyServersFilepath,
}
}
func (s *Storage) overrideWith(other Storage) {
s.Filepath = gosettings.OverrideWithPointer(s.Filepath, other.Filepath)
s.ServersEnabled = gosettings.OverrideWithPointer(s.ServersEnabled, other.ServersEnabled)
s.ServersPath = gosettings.OverrideWithComparable(s.ServersPath, other.ServersPath)
s.LegacyServersFilepath = gosettings.OverrideWithComparable(s.LegacyServersFilepath, other.LegacyServersFilepath)
}
func (s *Storage) setDefaults() {
const defaultFilepath = "/gluetun/servers.json"
s.Filepath = gosettings.DefaultPointer(s.Filepath, defaultFilepath)
const defaultLegacyServersFilepath = "/gluetun/servers.json"
func (s *Storage) SetDefaults() {
s.ServersEnabled = gosettings.DefaultPointer(s.ServersEnabled, true)
const defaultServersPath = "/gluetun/servers/"
s.ServersPath = gosettings.DefaultComparable(s.ServersPath, defaultServersPath)
s.LegacyServersFilepath = gosettings.DefaultComparable(s.LegacyServersFilepath, defaultLegacyServersFilepath)
}
func (s Storage) String() string {
@@ -45,15 +64,33 @@ func (s Storage) String() string {
}
func (s Storage) toLinesNode() (node *gotree.Node) {
if *s.Filepath == "" {
if !*s.ServersEnabled {
return gotree.New("Storage settings: disabled")
}
node = gotree.New("Storage settings:")
node.Appendf("Filepath: %s", *s.Filepath)
node.Appendf("Servers directory path: %s", s.ServersPath)
if s.LegacyServersFilepath != defaultLegacyServersFilepath {
node.Appendf("Legacy servers filepath: %s", s.LegacyServersFilepath)
}
return node
}
func (s *Storage) read(r *reader.Reader) (err error) {
s.Filepath = r.Get("STORAGE_FILEPATH", reader.AcceptEmpty(true))
func (s *Storage) Read(r *reader.Reader) (err error) {
// Retro-compatibility:
// TODO v4: remove support for STORAGE_FILEPATH
filePath := r.Get("STORAGE_FILEPATH", reader.AcceptEmpty(true), reader.IsRetro("STORAGE_SERVERS_DIRECTORY_PATH"))
if filePath != nil {
if *filePath == "" {
s.ServersEnabled = ptrTo(false)
} else {
s.LegacyServersFilepath = *filePath
}
} else {
s.ServersEnabled, err = r.BoolPtr("STORAGE_SERVERS_ENABLED")
if err != nil {
return err
}
s.ServersPath = r.String("STORAGE_SERVERS_DIRECTORY_PATH")
}
return nil
}
+25 -34
View File
@@ -1,6 +1,7 @@
package settings
import (
"errors"
"fmt"
"slices"
"strings"
@@ -21,10 +22,6 @@ type Updater struct {
// 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.
@@ -32,6 +29,9 @@ type Updater struct {
// Providers is the list of VPN service providers
// to update server information for.
Providers []string
// PreferDirectDownload is whether to prefer direct download of
// server data from Github (recommended).
PreferDirectDownload *bool
// ProtonEmail is the email to authenticate with the Proton API.
ProtonEmail *string
// ProtonPassword is the password to authenticate with the Proton API.
@@ -41,20 +41,20 @@ type Updater struct {
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)
return fmt.Errorf("VPN server data updater period is too small: "+
"%d must be larger than %s", *u.Period, minPeriod)
}
if u.MinRatio <= 0 || u.MinRatio > 1 {
return fmt.Errorf("%w: %.2f must be between 0+ and 1",
ErrMinRatioNotValid, u.MinRatio)
return fmt.Errorf("minimum ratio is not valid: "+
"%.2f must be between 0+ and 1", 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)
return fmt.Errorf("VPN provider name is not valid: %w", err)
}
if provider == providers.Protonvpn {
@@ -62,9 +62,9 @@ func (u Updater) Validate() (err error) {
if authenticatedAPI {
switch {
case *u.ProtonEmail == "":
return fmt.Errorf("%w", ErrUpdaterProtonEmailMissing)
return errors.New("proton email is missing")
case *u.ProtonPassword == "":
return fmt.Errorf("%w", ErrUpdaterProtonPasswordMissing)
return errors.New("proton password is missing")
}
}
}
@@ -75,12 +75,12 @@ func (u Updater) Validate() (err error) {
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),
Period: gosettings.CopyPointer(u.Period),
MinRatio: u.MinRatio,
Providers: gosettings.CopySlice(u.Providers),
PreferDirectDownload: gosettings.CopyPointer(u.PreferDirectDownload),
ProtonEmail: gosettings.CopyPointer(u.ProtonEmail),
ProtonPassword: gosettings.CopyPointer(u.ProtonPassword),
}
}
@@ -89,16 +89,15 @@ func (u *Updater) copy() (copied Updater) {
// 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.PreferDirectDownload = gosettings.OverrideWithPointer(u.PreferDirectDownload, other.PreferDirectDownload)
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
@@ -110,6 +109,7 @@ func (u *Updater) SetDefaults(vpnProvider string) {
}
// Set these to empty strings to avoid nil pointer panics
u.PreferDirectDownload = gosettings.DefaultPointer(u.PreferDirectDownload, false)
u.ProtonEmail = gosettings.DefaultPointer(u.ProtonEmail, "")
u.ProtonPassword = gosettings.DefaultPointer(u.ProtonPassword, "")
}
@@ -125,9 +125,9 @@ func (u Updater) toLinesNode() (node *gotree.Node) {
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, ", "))
node.Appendf("Prefer direct download: %s", gosettings.BoolToYesNo(u.PreferDirectDownload))
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))
@@ -142,11 +142,6 @@ func (u *Updater) read(r *reader.Reader) (err error) {
return err
}
u.DNSAddress, err = readUpdaterDNSAddress()
if err != nil {
return err
}
u.MinRatio, err = r.Float64("UPDATER_MIN_RATIO")
if err != nil {
return err
@@ -154,6 +149,11 @@ func (u *Updater) read(r *reader.Reader) (err error) {
u.Providers = r.CSV("UPDATER_VPN_SERVICE_PROVIDERS")
u.PreferDirectDownload, err = r.BoolPtr("UPDATER_PREFER_DIRECT_DOWNLOAD")
if err != nil {
return err
}
u.ProtonEmail = r.Get("UPDATER_PROTONVPN_EMAIL")
if u.ProtonEmail == nil {
protonUsername := r.String("UPDATER_PROTONVPN_USERNAME", reader.IsRetro("UPDATER_PROTONVPN_EMAIL"))
@@ -166,12 +166,3 @@ func (u *Updater) read(r *reader.Reader) (err error) {
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
}
+60 -13
View File
@@ -16,17 +16,28 @@ type VPN struct {
// empty string in the internal state.
Type string `json:"type"`
Provider Provider `json:"provider"`
AmneziaWg AmneziaWg `json:"amneziawg"`
OpenVPN OpenVPN `json:"openvpn"`
Wireguard Wireguard `json:"wireguard"`
PMTUD PMTUD `json:"pmtud"`
// UpCommand is the command to use when the VPN connection is up.
// It can be the empty string to indicate not to run a command.
// It cannot be nil in the internal state.
UpCommand *string `json:"up_command"`
// DownCommand is the command to use after the VPN connection goes down.
// It can be the empty string to indicate to NOT run a command.
// It cannot be nil in the internal state.
DownCommand *string `json:"down_command"`
}
// Validate validates VPN settings, using the filter choices getter (aka servers data storage),
// and if IPv6 is supported or not.
// TODO v4 remove pointer for receiver (because of Surfshark).
func (v *VPN) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Supported bool, warner Warner) (err error) {
// Validate Type
validVPNTypes := []string{vpn.OpenVPN, vpn.Wireguard}
validVPNTypes := []string{vpn.AmneziaWg, vpn.OpenVPN, vpn.Wireguard}
if err = validate.IsOneOf(v.Type, validVPNTypes...); err != nil {
return fmt.Errorf("%w: %w", ErrVPNTypeNotValid, err)
return fmt.Errorf("VPN type is not valid: %w", err)
}
err = v.Provider.validate(v.Type, filterChoicesGetter, warner)
@@ -34,13 +45,20 @@ func (v *VPN) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Supported bo
return fmt.Errorf("provider settings: %w", err)
}
if v.Type == vpn.OpenVPN {
switch v.Type {
case vpn.AmneziaWg:
err = v.AmneziaWg.validate(v.Provider.Name, ipv6Supported)
if err != nil {
return fmt.Errorf("AmneziaWG settings: %w", err)
}
case vpn.OpenVPN:
err := v.OpenVPN.validate(v.Provider.Name)
if err != nil {
return fmt.Errorf("OpenVPN settings: %w", err)
}
} else {
err := v.Wireguard.validate(v.Provider.Name, ipv6Supported)
case vpn.Wireguard:
const amneziawg = false
err := v.Wireguard.validate(v.Provider.Name, ipv6Supported, amneziawg)
if err != nil {
return fmt.Errorf("Wireguard settings: %w", err)
}
@@ -56,28 +74,37 @@ func (v *VPN) Validate(filterChoicesGetter FilterChoicesGetter, ipv6Supported bo
func (v *VPN) Copy() (copied VPN) {
return VPN{
Type: v.Type,
Provider: v.Provider.copy(),
OpenVPN: v.OpenVPN.copy(),
Wireguard: v.Wireguard.copy(),
PMTUD: v.PMTUD.copy(),
Type: v.Type,
Provider: v.Provider.copy(),
AmneziaWg: v.AmneziaWg.copy(),
OpenVPN: v.OpenVPN.copy(),
Wireguard: v.Wireguard.copy(),
PMTUD: v.PMTUD.copy(),
UpCommand: gosettings.CopyPointer(v.UpCommand),
DownCommand: gosettings.CopyPointer(v.DownCommand),
}
}
func (v *VPN) OverrideWith(other VPN) {
v.Type = gosettings.OverrideWithComparable(v.Type, other.Type)
v.Provider.overrideWith(other.Provider)
v.AmneziaWg.overrideWith(other.AmneziaWg)
v.OpenVPN.overrideWith(other.OpenVPN)
v.Wireguard.overrideWith(other.Wireguard)
v.PMTUD.overrideWith(other.PMTUD)
v.UpCommand = gosettings.OverrideWithPointer(v.UpCommand, other.UpCommand)
v.DownCommand = gosettings.OverrideWithPointer(v.DownCommand, other.DownCommand)
}
func (v *VPN) setDefaults() {
v.Type = gosettings.DefaultComparable(v.Type, vpn.OpenVPN)
v.Provider.setDefaults()
v.AmneziaWg.setDefaults(v.Provider.Name)
v.OpenVPN.setDefaults(v.Provider.Name)
v.Wireguard.setDefaults(v.Provider.Name)
v.PMTUD.setDefaults()
v.UpCommand = gosettings.DefaultPointer(v.UpCommand, "")
v.DownCommand = gosettings.DefaultPointer(v.DownCommand, "")
}
func (v VPN) String() string {
@@ -89,13 +116,23 @@ func (v VPN) toLinesNode() (node *gotree.Node) {
node.AppendNode(v.Provider.toLinesNode())
if v.Type == vpn.OpenVPN {
switch v.Type {
case vpn.AmneziaWg:
node.AppendNode(v.AmneziaWg.toLinesNode())
case vpn.OpenVPN:
node.AppendNode(v.OpenVPN.toLinesNode())
} else {
case vpn.Wireguard:
node.AppendNode(v.Wireguard.toLinesNode())
}
node.AppendNode(v.PMTUD.toLinesNode())
if *v.UpCommand != "" {
node.Appendf("Up command: %s", *v.UpCommand)
}
if *v.DownCommand != "" {
node.Appendf("Down command: %s", *v.DownCommand)
}
return node
}
@@ -107,12 +144,18 @@ func (v *VPN) read(r *reader.Reader) (err error) {
return fmt.Errorf("VPN provider: %w", err)
}
err = v.AmneziaWg.read(r)
if err != nil {
return fmt.Errorf("AmneziaWG: %w", err)
}
err = v.OpenVPN.read(r)
if err != nil {
return fmt.Errorf("OpenVPN: %w", err)
}
err = v.Wireguard.read(r)
const amneziawg = false
err = v.Wireguard.read(r, amneziawg)
if err != nil {
return fmt.Errorf("wireguard: %w", err)
}
@@ -122,5 +165,9 @@ func (v *VPN) read(r *reader.Reader) (err error) {
return fmt.Errorf("PMTUD: %w", err)
}
v.UpCommand = r.Get("VPN_UP_COMMAND", reader.ForceLowercase(false))
v.DownCommand = r.Get("VPN_DOWN_COMMAND", reader.ForceLowercase(false))
return nil
}
+57 -46
View File
@@ -1,13 +1,13 @@
package settings
import (
"errors"
"fmt"
"net/netip"
"regexp"
"strings"
"time"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
@@ -46,31 +46,23 @@ type Wireguard struct {
// It defaults to "auto" and cannot be the empty string
// in the internal state.
Implementation string `json:"implementation"`
// GSO enables wireguard-go's GRO/GSO batched TUN I/O by creating
// the WireGuard TUN device with IFF_VNET_HDR. It should be disabled
// on kernels (e.g. certain NAS devices) that claim IFF_VNET_HDR
// support but return EINVAL when wireguard-go writes GRO-coalesced
// packets with virtio_net_hdr structs under load.
// It defaults to true and cannot be nil in the internal state.
GSO *bool `json:"gso"`
}
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, ipv6Supported bool) (err error) {
if !helpers.IsOneOf(vpnProvider,
providers.Airvpn,
providers.Custom,
providers.Fastestvpn,
providers.Ivpn,
providers.Mullvad,
providers.Nordvpn,
providers.Protonvpn,
providers.Surfshark,
providers.Windscribe,
) {
// do not validate for VPN provider not supporting Wireguard
return nil
}
// It should only be ran if the VPN type chosen is Wireguard or AmneziaWg.
func (w Wireguard) validate(vpnProvider string, ipv6Supported, amneziawg bool) (err error) {
// Validate PrivateKey
if *w.PrivateKey == "" {
return fmt.Errorf("%w", ErrWireguardPrivateKeyNotSet)
return errors.New("private key is not set")
}
_, err = wgtypes.ParseKey(*w.PrivateKey)
if err != nil {
@@ -84,7 +76,7 @@ func (w Wireguard) validate(vpnProvider string, ipv6Supported bool) (err error)
if vpnProvider == providers.Airvpn {
if *w.PreSharedKey == "" {
return fmt.Errorf("%w", ErrWireguardPreSharedKeyNotSet)
return errors.New("pre-shared key is not set")
}
}
@@ -98,17 +90,15 @@ func (w Wireguard) validate(vpnProvider string, ipv6Supported bool) (err error)
// Validate Addresses
if len(w.Addresses) == 0 {
return fmt.Errorf("%w", ErrWireguardInterfaceAddressNotSet)
return errors.New("interface address is not set")
}
for i, ipNet := range w.Addresses {
if !ipNet.IsValid() {
return fmt.Errorf("%w: for address at index %d",
ErrWireguardInterfaceAddressNotSet, i)
return fmt.Errorf("interface address is not set: for address at index %d", i)
}
if !ipv6Supported && ipNet.Addr().Is6() {
return fmt.Errorf("%w: address %s",
ErrWireguardInterfaceAddressIPv6, ipNet.String())
return fmt.Errorf("interface address is IPv6 but IPv6 is not supported: address %s", ipNet.String())
}
}
@@ -116,29 +106,28 @@ func (w Wireguard) validate(vpnProvider string, ipv6Supported bool) (err error)
// WARNING: do not check for IPv6 networks in the allowed IPs,
// the wireguard code will take care to ignore it.
if len(w.AllowedIPs) == 0 {
return fmt.Errorf("%w", ErrWireguardAllowedIPsNotSet)
return errors.New("allowed IPs is not set")
}
for i, allowedIP := range w.AllowedIPs {
if !allowedIP.IsValid() {
return fmt.Errorf("%w: for allowed ip %d of %d",
ErrWireguardAllowedIPNotSet, i+1, len(w.AllowedIPs))
return fmt.Errorf("allowed IP is not set: for allowed ip %d of %d", i+1, len(w.AllowedIPs))
}
}
if *w.PersistentKeepaliveInterval < 0 {
return fmt.Errorf("%w: %s", ErrWireguardKeepAliveNegative,
*w.PersistentKeepaliveInterval)
return fmt.Errorf("persistent keep alive interval is negative: %s", *w.PersistentKeepaliveInterval)
}
// Validate interface
if !regexpInterfaceName.MatchString(w.Interface) {
return fmt.Errorf("%w: '%s' does not match regex '%s'",
ErrWireguardInterfaceNotValid, w.Interface, regexpInterfaceName)
return fmt.Errorf("interface name is not valid: '%s' does not match regex '%s'", w.Interface, regexpInterfaceName)
}
validImplementations := []string{"auto", "userspace", "kernelspace"}
if err := validate.IsOneOf(w.Implementation, validImplementations...); err != nil {
return fmt.Errorf("%w: %w", ErrWireguardImplementationNotValid, err)
if !amneziawg { // amneziawg should have its own Implementation field and ignore this one
validImplementations := []string{"auto", "userspace", "kernelspace"}
if err := validate.IsOneOf(w.Implementation, validImplementations...); err != nil {
return fmt.Errorf("implementation is not valid: %w", err)
}
}
return nil
@@ -154,6 +143,7 @@ func (w *Wireguard) copy() (copied Wireguard) {
Interface: w.Interface,
MTU: w.MTU,
Implementation: w.Implementation,
GSO: gosettings.CopyPointer(w.GSO),
}
}
@@ -167,6 +157,7 @@ func (w *Wireguard) overrideWith(other Wireguard) {
w.Interface = gosettings.OverrideWithComparable(w.Interface, other.Interface)
w.MTU = gosettings.OverrideWithComparable(w.MTU, other.MTU)
w.Implementation = gosettings.OverrideWithComparable(w.Implementation, other.Implementation)
w.GSO = gosettings.OverrideWithPointer(w.GSO, other.GSO)
}
func (w *Wireguard) setDefaults(vpnProvider string) {
@@ -191,6 +182,7 @@ func (w *Wireguard) setDefaults(vpnProvider string) {
w.Interface = gosettings.DefaultComparable(w.Interface, "wg0")
w.MTU = gosettings.DefaultPointer(w.MTU, 0)
w.Implementation = gosettings.DefaultComparable(w.Implementation, "auto")
w.GSO = gosettings.DefaultPointer(w.GSO, true)
}
func (w Wireguard) String() string {
@@ -235,24 +227,37 @@ func (w Wireguard) toLinesNode() (node *gotree.Node) {
node.Appendf("Implementation: %s", w.Implementation)
}
if !*w.GSO {
node.Append("GSO disabled")
}
return node
}
func (w *Wireguard) read(r *reader.Reader) (err error) {
w.PrivateKey = r.Get("WIREGUARD_PRIVATE_KEY", reader.ForceLowercase(false))
w.PreSharedKey = r.Get("WIREGUARD_PRESHARED_KEY", reader.ForceLowercase(false))
func (w *Wireguard) read(r *reader.Reader, amneziaWG bool) (err error) {
prefix := "WIREGUARD"
if amneziaWG {
prefix = "AMNEZIAWG"
}
w.PrivateKey = r.Get(prefix+"_PRIVATE_KEY", reader.ForceLowercase(false))
w.PreSharedKey = r.Get(prefix+"_PRESHARED_KEY", reader.ForceLowercase(false))
w.Interface = r.String("VPN_INTERFACE",
reader.RetroKeys("WIREGUARD_INTERFACE"), reader.ForceLowercase(false))
w.Implementation = r.String("WIREGUARD_IMPLEMENTATION")
reader.RetroKeys(prefix+"_INTERFACE"), reader.ForceLowercase(false))
addressStrings := r.CSV("WIREGUARD_ADDRESSES", reader.RetroKeys("WIREGUARD_ADDRESS"))
if !amneziaWG {
w.Implementation = r.String("WIREGUARD_IMPLEMENTATION")
}
addressStrings := r.CSV(prefix+"_ADDRESSES", reader.RetroKeys(prefix+"_ADDRESS"))
// WARNING: do not initialize w.Addresses to an empty slice
// or the defaults for nordvpn will not work.
for _, addressString := range addressStrings {
if !strings.ContainsRune(addressString, '/') {
addressString = strings.TrimSpace(addressString)
if addressString == "" {
continue
} else if !strings.ContainsRune(addressString, '/') {
addressString += "/32"
}
addressString = strings.TrimSpace(addressString)
address, err := netip.ParsePrefix(addressString)
if err != nil {
return fmt.Errorf("parsing address: %w", err)
@@ -260,19 +265,25 @@ func (w *Wireguard) read(r *reader.Reader) (err error) {
w.Addresses = append(w.Addresses, address)
}
w.AllowedIPs, err = r.CSVNetipPrefixes("WIREGUARD_ALLOWED_IPS")
w.AllowedIPs, err = r.CSVNetipPrefixes(prefix + "_ALLOWED_IPS")
if err != nil {
return err // already wrapped
}
w.PersistentKeepaliveInterval, err = r.DurationPtr("WIREGUARD_PERSISTENT_KEEPALIVE_INTERVAL")
w.PersistentKeepaliveInterval, err = r.DurationPtr(prefix + "_PERSISTENT_KEEPALIVE_INTERVAL")
if err != nil {
return err
}
w.MTU, err = r.Uint32Ptr("WIREGUARD_MTU")
w.MTU, err = r.Uint32Ptr(prefix + "_MTU")
if err != nil {
return err
}
w.GSO, err = r.BoolPtr("WIREGUARD_GSO")
if err != nil {
return err
}
return nil
}
@@ -1,6 +1,7 @@
package settings
import (
"errors"
"fmt"
"net/netip"
@@ -44,7 +45,7 @@ func (w WireguardSelection) validate(vpnProvider string) (err error) {
// endpoint IP addresses are baked in
case providers.Custom:
if !w.EndpointIP.IsValid() || w.EndpointIP.IsUnspecified() {
return fmt.Errorf("%w", ErrWireguardEndpointIPNotSet)
return errors.New("endpoint IP is not set")
}
default: // Providers not supporting Wireguard
}
@@ -54,13 +55,13 @@ func (w WireguardSelection) validate(vpnProvider string) (err error) {
// EndpointPort is required
case providers.Custom:
if *w.EndpointPort == 0 {
return fmt.Errorf("%w", ErrWireguardEndpointPortNotSet)
return errors.New("endpoint port is not set")
}
// EndpointPort cannot be set
case providers.Fastestvpn, providers.Nordvpn,
providers.Protonvpn, providers.Surfshark:
if *w.EndpointPort != 0 {
return fmt.Errorf("%w", ErrWireguardEndpointPortSet)
return errors.New("endpoint port is set")
}
case providers.Airvpn, providers.Ivpn, providers.Mullvad, providers.Windscribe:
// EndpointPort is optional and can be 0
@@ -84,8 +85,7 @@ func (w WireguardSelection) validate(vpnProvider string) (err error) {
if err == nil {
break
}
return fmt.Errorf("%w: for VPN service provider %s: %w",
ErrWireguardEndpointPortNotAllowed, vpnProvider, err)
return fmt.Errorf("endpoint port is not allowed: for VPN service provider %s: %w", vpnProvider, err)
default: // Providers not supporting Wireguard
}
@@ -96,15 +96,14 @@ func (w WireguardSelection) validate(vpnProvider string) (err error) {
// public keys are baked in
case providers.Custom:
if w.PublicKey == "" {
return fmt.Errorf("%w", ErrWireguardPublicKeyNotSet)
return errors.New("public key is not set")
}
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 fmt.Errorf("public key is not valid: %s: %s", w.PublicKey, err)
}
}
@@ -152,18 +151,22 @@ func (w WireguardSelection) toLinesNode() (node *gotree.Node) {
return node
}
func (w *WireguardSelection) read(r *reader.Reader) (err error) {
w.EndpointIP, err = r.NetipAddr("WIREGUARD_ENDPOINT_IP", reader.RetroKeys("VPN_ENDPOINT_IP"))
func (w *WireguardSelection) read(r *reader.Reader, amneziaWG bool) (err error) {
prefix := "WIREGUARD"
if amneziaWG {
prefix = "AMNEZIAWG"
}
w.EndpointIP, err = r.NetipAddr(prefix+"_ENDPOINT_IP", reader.RetroKeys("VPN_ENDPOINT_IP"))
if err != nil {
return fmt.Errorf("%w - note this MUST be an IP address, "+
"see https://github.com/qdm12/gluetun/issues/788", err)
}
w.EndpointPort, err = r.Uint16Ptr("WIREGUARD_ENDPOINT_PORT", reader.RetroKeys("VPN_ENDPOINT_PORT"))
w.EndpointPort, err = r.Uint16Ptr(prefix+"_ENDPOINT_PORT", reader.RetroKeys("VPN_ENDPOINT_PORT"))
if err != nil {
return err
}
w.PublicKey = r.String("WIREGUARD_PUBLIC_KEY", reader.ForceLowercase(false))
w.PublicKey = r.String(prefix+"_PUBLIC_KEY", reader.ForceLowercase(false))
return nil
}