Compare commits

...

14 Commits

Author SHA1 Message Date
Quentin McGaw
4f0b8f7292 Merge branch 'master' into remove-keep-nameserver 2025-11-17 19:16:32 +00:00
Quentin McGaw
41cd8fb30d fix(storage): only log warning if flushing merged servers to file fails 2025-11-17 19:04:19 +00:00
dependabot[bot]
9ed6cd978d Chore(deps): Bump DavidAnson/markdownlint-cli2-action from 20 to 21 (#2984) 2025-11-17 19:57:57 +01:00
Quentin McGaw
c4b9d459ed fix(dns): fix panic when using DNS_KEEP_NAMESERVER 2025-11-17 17:59:18 +00:00
Quentin McGaw
6e99ca573e chore(storage): do not read/write to user file when updating in maintainer mode 2025-11-17 15:31:40 +00:00
Quentin McGaw
2cf4d6b469 fix(protonvpn/updater): ignore casing when comparing received username 2025-11-17 15:23:02 +00:00
Quentin McGaw
25b381e138 fix linting errors 2025-11-17 15:17:57 +00:00
Quentin McGaw
35b6b709b2 DNS_UPSTREAM_PLAIN_ADDRESSES option
- New CSV format with port, for example `ip1:port1,ip2:port2`
- retrocompatibility with `DNS_ADDRESS`. If set, force upstream type to plain and empty user-picked providers. 127.0.0.1 is now ignored since it's always set to this value internally.
- requires `DNS_UPSTREAM_TYPE=plain` must be set to use `DNS_UPSTREAM_PLAIN_ADDRESSES` (unless using retro `DNS_ADDRESS`)
- Warning log on using private upstream resolvers updated
2025-11-17 15:04:06 +00:00
Quentin McGaw
40ea51a3ae Remove DNS_KEEP_NAMESERVER (always off) 2025-11-17 13:05:33 +00:00
Quentin McGaw
1a93a41a55 Remove DNS_SERVER option (always on) 2025-11-17 13:00:01 +00:00
Quentin McGaw
a17776673b docs(readme): warning on "official" websites 2025-11-17 12:46:45 +00:00
Quentin McGaw
fcdba0a3cc feat(portforward): support {{PORT}} template variable 2025-11-16 00:18:01 +00:00
Quentin McGaw
4712d0cf79 change(healthcheck): bump tries and timeouts
- small periodic check from 10s+20s+30s to 5s+5s+5s+10s+10s+10s+15s+15s+15s+30s
- full periodic check from 10s+20s to 10s+15s+30s
2025-11-15 16:47:38 +00:00
Quentin McGaw
113c113615 feat(healthcheck): log duration for each failed attempt 2025-11-15 16:45:03 +00:00
23 changed files with 313 additions and 193 deletions

View File

@@ -20,7 +20,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- uses: DavidAnson/markdownlint-cli2-action@v20 - uses: DavidAnson/markdownlint-cli2-action@v21
with: with:
globs: "**.md" globs: "**.md"
config: .markdownlint-cli2.jsonc config: .markdownlint-cli2.jsonc

View File

@@ -167,7 +167,6 @@ ENV VPN_SERVICE_PROVIDER=pia \
HEALTH_ICMP_TARGET_IP=1.1.1.1 \ HEALTH_ICMP_TARGET_IP=1.1.1.1 \
HEALTH_RESTART_VPN=on \ HEALTH_RESTART_VPN=on \
# DNS # DNS
DNS_SERVER=on \
DNS_UPSTREAM_RESOLVER_TYPE=DoT \ DNS_UPSTREAM_RESOLVER_TYPE=DoT \
DNS_UPSTREAM_RESOLVERS=cloudflare \ DNS_UPSTREAM_RESOLVERS=cloudflare \
DNS_BLOCK_IPS= \ DNS_BLOCK_IPS= \
@@ -180,8 +179,7 @@ ENV VPN_SERVICE_PROVIDER=pia \
DNS_UNBLOCK_HOSTNAMES= \ DNS_UNBLOCK_HOSTNAMES= \
DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES= \ DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES= \
DNS_UPDATE_PERIOD=24h \ DNS_UPDATE_PERIOD=24h \
DNS_ADDRESS=127.0.0.1 \ DNS_UPSTREAM_PLAIN_ADDRESSES= \
DNS_KEEP_NAMESERVER=off \
# HTTP proxy # HTTP proxy
HTTPPROXY= \ HTTPPROXY= \
HTTPPROXY_LOG=off \ HTTPPROXY_LOG=off \

View File

@@ -1,5 +1,7 @@
# Gluetun VPN client # Gluetun VPN client
⚠️ This and [gluetun-wiki](https://github.com/qdm12/gluetun-wiki) are the only websites for Gluetun, other websites claiming to be official are scams ⚠️
Lightweight swiss-army-knife-like VPN client to multiple VPN service providers Lightweight swiss-army-knife-like VPN client to multiple VPN service providers
![Title image](https://raw.githubusercontent.com/qdm12/gluetun/master/title.svg) ![Title image](https://raw.githubusercontent.com/qdm12/gluetun/master/title.svg)

View File

@@ -7,3 +7,4 @@ func newNoopLogger() *noopLogger {
} }
func (l *noopLogger) Info(string) {} func (l *noopLogger) Info(string) {}
func (l *noopLogger) Warn(string) {}

View File

@@ -81,7 +81,11 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
return fmt.Errorf("options validation failed: %w", err) return fmt.Errorf("options validation failed: %w", err)
} }
storage, err := storage.New(logger, constants.ServersData) serversDataPath := constants.ServersData
if maintainerMode {
serversDataPath = ""
}
storage, err := storage.New(logger, serversDataPath)
if err != nil { if err != nil {
return fmt.Errorf("creating servers storage: %w", err) return fmt.Errorf("creating servers storage: %w", err)
} }

View File

@@ -14,6 +14,10 @@ func readObsolete(r *reader.Reader) (warnings []string) {
"DOT_VALIDATION_LOGLEVEL": "DOT_VALIDATION_LOGLEVEL is obsolete because DNSSEC validation is not implemented.", "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_INITIAL": "HEALTH_VPN_DURATION_INITIAL is obsolete",
"HEALTH_VPN_DURATION_ADDITION": "HEALTH_VPN_DURATION_ADDITION is obsolete", "HEALTH_VPN_DURATION_ADDITION": "HEALTH_VPN_DURATION_ADDITION is obsolete",
"DNS_SERVER": "DNS_SERVER is obsolete because the forwarding server is always enabled.",
"DOT": "DOT is obsolete because the forwarding server is always enabled.",
"DNS_KEEP_NAMESERVER": "DNS_KEEP_NAMESERVER is obsolete because the forwarding server is always used and " +
"forwards local names to private DNS resolvers found in /etc/resolv.conf",
} }
sortedKeys := maps.Keys(keyToMessage) sortedKeys := maps.Keys(keyToMessage)
slices.Sort(sortedKeys) slices.Sort(sortedKeys)

View File

@@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/netip" "net/netip"
"slices"
"time" "time"
"github.com/qdm12/dns/v2/pkg/provider" "github.com/qdm12/dns/v2/pkg/provider"
@@ -13,20 +14,25 @@ import (
"github.com/qdm12/gotree" "github.com/qdm12/gotree"
) )
const (
DNSUpstreamTypeDot = "dot"
DNSUpstreamTypeDoh = "doh"
DNSUpstreamTypePlain = "plain"
)
// DNS contains settings to configure DNS. // DNS contains settings to configure DNS.
type DNS struct { type DNS struct {
// ServerEnabled is true if the server should be running // UpstreamType can be [dnsUpstreamTypeDot], [dnsUpstreamTypeDoh]
// and used. It defaults to true, and cannot be nil // or [dnsUpstreamTypePlain]. It defaults to [dnsUpstreamTypeDot].
// in the internal state.
ServerEnabled *bool
// UpstreamType can be dot or plain, and defaults to dot.
UpstreamType string `json:"upstream_type"` UpstreamType string `json:"upstream_type"`
// UpdatePeriod is the period to update DNS block lists. // UpdatePeriod is the period to update DNS block lists.
// It can be set to 0 to disable the update. // It can be set to 0 to disable the update.
// It defaults to 24h and cannot be nil in // It defaults to 24h and cannot be nil in
// the internal state. // the internal state.
UpdatePeriod *time.Duration UpdatePeriod *time.Duration
// Providers is a list of DNS providers // Providers is a list of DNS providers.
// It defaults to either ["cloudflare"] or [] if the
// UpstreamPlainAddresses field is set.
Providers []string `json:"providers"` Providers []string `json:"providers"`
// Caching is true if the server should cache // Caching is true if the server should cache
// DNS responses. // DNS responses.
@@ -36,32 +42,23 @@ type DNS struct {
// Blacklist contains settings to configure the filter // Blacklist contains settings to configure the filter
// block lists. // block lists.
Blacklist DNSBlacklist Blacklist DNSBlacklist
// ServerAddress is the DNS server to use inside // UpstreamPlainAddresses are the upstream plaintext DNS resolver
// the Go program and for the system. // addresses to use by the built-in DNS server forwarder.
// It defaults to '127.0.0.1' to be used with the // Note, if the upstream type is [dnsUpstreamTypePlain] these are merged
// local server. It cannot be the zero value in the internal // together with provider names set in the Providers field.
// state. // If this field is set, the Providers field will default to the empty slice.
ServerAddress netip.Addr UpstreamPlainAddresses []netip.AddrPort
// 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
} }
var ( var (
ErrDNSUpstreamTypeNotValid = errors.New("DNS upstream type is not valid") ErrDNSUpstreamTypeNotValid = errors.New("DNS upstream type is not valid")
ErrDNSUpdatePeriodTooShort = errors.New("update period is too short") ErrDNSUpdatePeriodTooShort = errors.New("update period is too short")
ErrDNSUpstreamPlainNoIPv6 = errors.New("upstream plain addresses do not contain any IPv6 address")
ErrDNSUpstreamPlainNoIPv4 = errors.New("upstream plain addresses do not contain any IPv4 address")
) )
func (d DNS) validate() (err error) { func (d DNS) validate() (err error) {
if !helpers.IsOneOf(d.UpstreamType, "dot", "doh", "plain") { if !helpers.IsOneOf(d.UpstreamType, DNSUpstreamTypeDot, DNSUpstreamTypeDoh, DNSUpstreamTypePlain) {
return fmt.Errorf("%w: %s", ErrDNSUpstreamTypeNotValid, d.UpstreamType) return fmt.Errorf("%w: %s", ErrDNSUpstreamTypeNotValid, d.UpstreamType)
} }
@@ -79,6 +76,18 @@ func (d DNS) validate() (err error) {
} }
} }
if d.UpstreamType == DNSUpstreamTypePlain {
if *d.IPv6 && !slices.ContainsFunc(d.UpstreamPlainAddresses, func(addrPort netip.AddrPort) bool {
return addrPort.Addr().Is6()
}) {
return fmt.Errorf("%w: in %d addresses", ErrDNSUpstreamPlainNoIPv6, len(d.UpstreamPlainAddresses))
} else if !slices.ContainsFunc(d.UpstreamPlainAddresses, func(addrPort netip.AddrPort) bool {
return addrPort.Addr().Is4()
}) {
return fmt.Errorf("%w: in %d addresses", ErrDNSUpstreamPlainNoIPv4, len(d.UpstreamPlainAddresses))
}
}
err = d.Blacklist.validate() err = d.Blacklist.validate()
if err != nil { if err != nil {
return err return err
@@ -89,15 +98,13 @@ func (d DNS) validate() (err error) {
func (d *DNS) Copy() (copied DNS) { func (d *DNS) Copy() (copied DNS) {
return DNS{ return DNS{
ServerEnabled: gosettings.CopyPointer(d.ServerEnabled),
UpstreamType: d.UpstreamType, UpstreamType: d.UpstreamType,
UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod), UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod),
Providers: gosettings.CopySlice(d.Providers), Providers: gosettings.CopySlice(d.Providers),
Caching: gosettings.CopyPointer(d.Caching), Caching: gosettings.CopyPointer(d.Caching),
IPv6: gosettings.CopyPointer(d.IPv6), IPv6: gosettings.CopyPointer(d.IPv6),
Blacklist: d.Blacklist.copy(), Blacklist: d.Blacklist.copy(),
ServerAddress: d.ServerAddress, UpstreamPlainAddresses: d.UpstreamPlainAddresses,
KeepNameserver: gosettings.CopyPointer(d.KeepNameserver),
} }
} }
@@ -105,20 +112,17 @@ func (d *DNS) Copy() (copied DNS) {
// settings object with any field set in the other // settings object with any field set in the other
// settings. // settings.
func (d *DNS) overrideWith(other DNS) { func (d *DNS) overrideWith(other DNS) {
d.ServerEnabled = gosettings.OverrideWithPointer(d.ServerEnabled, other.ServerEnabled)
d.UpstreamType = gosettings.OverrideWithComparable(d.UpstreamType, other.UpstreamType) d.UpstreamType = gosettings.OverrideWithComparable(d.UpstreamType, other.UpstreamType)
d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod) d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod)
d.Providers = gosettings.OverrideWithSlice(d.Providers, other.Providers) d.Providers = gosettings.OverrideWithSlice(d.Providers, other.Providers)
d.Caching = gosettings.OverrideWithPointer(d.Caching, other.Caching) d.Caching = gosettings.OverrideWithPointer(d.Caching, other.Caching)
d.IPv6 = gosettings.OverrideWithPointer(d.IPv6, other.IPv6) d.IPv6 = gosettings.OverrideWithPointer(d.IPv6, other.IPv6)
d.Blacklist.overrideWith(other.Blacklist) d.Blacklist.overrideWith(other.Blacklist)
d.ServerAddress = gosettings.OverrideWithValidator(d.ServerAddress, other.ServerAddress) d.UpstreamPlainAddresses = gosettings.OverrideWithSlice(d.UpstreamPlainAddresses, other.UpstreamPlainAddresses)
d.KeepNameserver = gosettings.OverrideWithPointer(d.KeepNameserver, other.KeepNameserver)
} }
func (d *DNS) setDefaults() { func (d *DNS) setDefaults() {
d.ServerEnabled = gosettings.DefaultPointer(d.ServerEnabled, true) d.UpstreamType = gosettings.DefaultComparable(d.UpstreamType, DNSUpstreamTypeDot)
d.UpstreamType = gosettings.DefaultComparable(d.UpstreamType, "dot")
const defaultUpdatePeriod = 24 * time.Hour const defaultUpdatePeriod = 24 * time.Hour
d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod) d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod)
d.Providers = gosettings.DefaultSlice(d.Providers, []string{ d.Providers = gosettings.DefaultSlice(d.Providers, []string{
@@ -127,27 +131,54 @@ func (d *DNS) setDefaults() {
d.Caching = gosettings.DefaultPointer(d.Caching, true) d.Caching = gosettings.DefaultPointer(d.Caching, true)
d.IPv6 = gosettings.DefaultPointer(d.IPv6, false) d.IPv6 = gosettings.DefaultPointer(d.IPv6, false)
d.Blacklist.setDefaults() d.Blacklist.setDefaults()
d.ServerAddress = gosettings.DefaultValidator(d.ServerAddress, d.UpstreamPlainAddresses = gosettings.DefaultSlice(d.UpstreamPlainAddresses, []netip.AddrPort{})
netip.AddrFrom4([4]byte{127, 0, 0, 1})) }
d.KeepNameserver = gosettings.DefaultPointer(d.KeepNameserver, false)
func defaultDNSProviders() []string {
return []string{
provider.Cloudflare().Name,
}
} }
func (d DNS) GetFirstPlaintextIPv4() (ipv4 netip.Addr) { func (d DNS) GetFirstPlaintextIPv4() (ipv4 netip.Addr) {
localhost := netip.AddrFrom4([4]byte{127, 0, 0, 1}) if d.UpstreamType == DNSUpstreamTypePlain {
if d.ServerAddress.Compare(localhost) != 0 && d.ServerAddress.Is4() { for _, addrPort := range d.UpstreamPlainAddresses {
return d.ServerAddress if addrPort.Addr().Is4() {
return addrPort.Addr()
}
}
} }
ipv4 = findPlainIPv4InProviders(d.Providers)
if ipv4.IsValid() {
return ipv4
}
// Either:
// - all upstream plain addresses are IPv6 and no provider is set
// - all providers set do not have a plaintext IPv4 address
ipv4 = findPlainIPv4InProviders(defaultDNSProviders())
if !ipv4.IsValid() {
panic("no plaintext IPv4 address found in default DNS providers")
}
return ipv4
}
func findPlainIPv4InProviders(providerNames []string) netip.Addr {
providers := provider.NewProviders() providers := provider.NewProviders()
provider, err := providers.Get(d.Providers[0]) for _, name := range providerNames {
provider, err := providers.Get(name)
if err != nil { if err != nil {
// Settings should be validated before calling this function, // Settings should be validated before calling this function,
// so an error happening here is a programming error. // so an error happening here is a programming error.
panic(err) panic(err)
} }
if len(provider.Plain.IPv4) > 0 {
return provider.Plain.IPv4[0].Addr() return provider.Plain.IPv4[0].Addr()
} }
}
return netip.Addr{}
}
func (d DNS) String() string { func (d DNS) String() string {
return d.toLinesNode().String() return d.toLinesNode().String()
@@ -155,23 +186,23 @@ func (d DNS) String() string {
func (d DNS) toLinesNode() (node *gotree.Node) { func (d DNS) toLinesNode() (node *gotree.Node) {
node = gotree.New("DNS settings:") 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 {
return node
}
node.Appendf("Upstream resolver type: %s", d.UpstreamType) node.Appendf("Upstream resolver type: %s", d.UpstreamType)
upstreamResolvers := node.Append("Upstream resolvers:") upstreamResolvers := node.Append("Upstream resolvers:")
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")
}
} else {
for _, provider := range d.Providers { for _, provider := range d.Providers {
upstreamResolvers.Append(provider) upstreamResolvers.Append(provider)
} }
}
node.Appendf("Caching: %s", gosettings.BoolToYesNo(d.Caching)) node.Appendf("Caching: %s", gosettings.BoolToYesNo(d.Caching))
node.Appendf("IPv6: %s", gosettings.BoolToYesNo(d.IPv6)) node.Appendf("IPv6: %s", gosettings.BoolToYesNo(d.IPv6))
@@ -188,11 +219,6 @@ func (d DNS) toLinesNode() (node *gotree.Node) {
} }
func (d *DNS) read(r *reader.Reader) (err error) { func (d *DNS) read(r *reader.Reader) (err error) {
d.ServerEnabled, err = r.BoolPtr("DNS_SERVER", reader.RetroKeys("DOT"))
if err != nil {
return err
}
d.UpstreamType = r.String("DNS_UPSTREAM_RESOLVER_TYPE") d.UpstreamType = r.String("DNS_UPSTREAM_RESOLVER_TYPE")
d.UpdatePeriod, err = r.DurationPtr("DNS_UPDATE_PERIOD") d.UpdatePeriod, err = r.DurationPtr("DNS_UPDATE_PERIOD")
@@ -217,15 +243,43 @@ func (d *DNS) read(r *reader.Reader) (err error) {
return err return err
} }
d.ServerAddress, err = r.NetipAddr("DNS_ADDRESS", reader.RetroKeys("DNS_PLAINTEXT_ADDRESS")) err = d.readUpstreamPlainAddresses(r)
if err != nil {
return err
}
d.KeepNameserver, err = r.BoolPtr("DNS_KEEP_NAMESERVER")
if err != nil { if err != nil {
return err return err
} }
return nil 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_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 AND
// clear any user picked providers, 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
d.Providers = []string{}
return nil
}

View File

@@ -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")
}

View File

@@ -2,7 +2,6 @@ package settings
import ( import (
"fmt" "fmt"
"net/netip"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers" "github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants/providers" "github.com/qdm12/gluetun/internal/constants/providers"
@@ -174,13 +173,11 @@ func (s Settings) Warnings() (warnings []string) {
"by creating an issue, attaching the new certificate and we will update Gluetun.") "by creating an issue, attaching the new certificate and we will update Gluetun.")
} }
// TODO remove in v4 for _, upstreamAddress := range s.DNS.UpstreamPlainAddresses {
if s.DNS.ServerAddress.Unmap().Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) != 0 { if upstreamAddress.Addr().IsPrivate() {
warnings = append(warnings, "DNS address is set to "+s.DNS.ServerAddress.String()+ warnings = append(warnings, "DNS upstream address "+upstreamAddress.String()+" is private: "+
" so the local forwarding DNS server will not be used."+ "DNS traffic might leak out of the VPN tunnel to that address.")
" 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.")
} }
return warnings return warnings

View File

@@ -38,9 +38,6 @@ func Test_Settings_String(t *testing.T) {
| ├── Run OpenVPN as: root | ├── Run OpenVPN as: root
| └── Verbosity level: 1 | └── Verbosity level: 1
├── DNS settings: ├── 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 resolver type: dot
| ├── Upstream resolvers: | ├── Upstream resolvers:
| | └── Cloudflare | | └── Cloudflare

View File

@@ -18,14 +18,8 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
return return
} }
if *l.GetSettings().KeepNameserver {
l.logger.Warn("⚠️⚠️⚠️ keeping the default container nameservers, " +
"this will likely leak DNS traffic outside the VPN " +
"and go through your container network DNS outside the VPN tunnel!")
} else {
const fallback = false const fallback = false
l.useUnencryptedDNS(fallback) l.useUnencryptedDNS(fallback)
}
select { select {
case <-l.start: case <-l.start:
@@ -38,14 +32,12 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
// Their values are to be used if DOT=off // Their values are to be used if DOT=off
var runError <-chan error var runError <-chan error
settings := l.GetSettings() for {
for !*settings.KeepNameserver && *settings.ServerEnabled {
var err error var err error
runError, err = l.setupServer(ctx) runError, err = l.setupServer(ctx)
if err == nil { if err == nil {
l.backoffTime = defaultBackoffTime l.backoffTime = defaultBackoffTime
l.logger.Info("ready") l.logger.Info("ready")
l.signalOrSetStatus(constants.Running)
break break
} }
@@ -60,14 +52,11 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
l.useUnencryptedDNS(fallback) l.useUnencryptedDNS(fallback)
} }
l.logAndWait(ctx, err) l.logAndWait(ctx, err)
settings = l.GetSettings()
} }
l.signalOrSetStatus(constants.Running)
settings = l.GetSettings()
if !*settings.KeepNameserver && !*settings.ServerEnabled {
const fallback = false const fallback = false
l.useUnencryptedDNS(fallback) l.useUnencryptedDNS(fallback)
}
l.userTrigger = false l.userTrigger = false

View File

@@ -26,31 +26,23 @@ func (l *Loop) SetSettings(ctx context.Context, settings settings.DNS) (
return l.state.SetSettings(ctx, settings) return l.state.SetSettings(ctx, settings)
} }
func buildServerSettings(settings settings.DNS, func buildServerSettings(userSettings settings.DNS,
filter *mapfilter.Filter, localResolvers []netip.Addr, filter *mapfilter.Filter, localResolvers []netip.Addr,
logger Logger) ( logger Logger) (
serverSettings server.Settings, err error, serverSettings server.Settings, err error,
) { ) {
serverSettings.Logger = logger serverSettings.Logger = logger
providersData := provider.NewProviders() upstreamResolvers := buildProviders(userSettings)
upstreamResolvers := make([]provider.Provider, len(settings.Providers))
for i := range settings.Providers {
var err error
upstreamResolvers[i], err = providersData.Get(settings.Providers[i])
if err != nil {
panic(err) // this should already had been checked
}
}
ipVersion := "ipv4" ipVersion := "ipv4"
if *settings.IPv6 { if *userSettings.IPv6 {
ipVersion = "ipv6" ipVersion = "ipv6"
} }
var dialer server.Dialer var dialer server.Dialer
switch settings.UpstreamType { switch userSettings.UpstreamType {
case "dot": case settings.DNSUpstreamTypeDot:
dialerSettings := dot.Settings{ dialerSettings := dot.Settings{
UpstreamResolvers: upstreamResolvers, UpstreamResolvers: upstreamResolvers,
IPVersion: ipVersion, IPVersion: ipVersion,
@@ -59,7 +51,7 @@ func buildServerSettings(settings settings.DNS,
if err != nil { if err != nil {
return server.Settings{}, fmt.Errorf("creating DNS over TLS dialer: %w", err) return server.Settings{}, fmt.Errorf("creating DNS over TLS dialer: %w", err)
} }
case "doh": case settings.DNSUpstreamTypeDoh:
dialerSettings := doh.Settings{ dialerSettings := doh.Settings{
UpstreamResolvers: upstreamResolvers, UpstreamResolvers: upstreamResolvers,
IPVersion: ipVersion, IPVersion: ipVersion,
@@ -68,7 +60,7 @@ func buildServerSettings(settings settings.DNS,
if err != nil { if err != nil {
return server.Settings{}, fmt.Errorf("creating DNS over HTTPS dialer: %w", err) return server.Settings{}, fmt.Errorf("creating DNS over HTTPS dialer: %w", err)
} }
case "plain": case settings.DNSUpstreamTypePlain:
dialerSettings := plain.Settings{ dialerSettings := plain.Settings{
UpstreamResolvers: upstreamResolvers, UpstreamResolvers: upstreamResolvers,
IPVersion: ipVersion, IPVersion: ipVersion,
@@ -78,11 +70,11 @@ func buildServerSettings(settings settings.DNS,
return server.Settings{}, fmt.Errorf("creating plain DNS dialer: %w", err) return server.Settings{}, fmt.Errorf("creating plain DNS dialer: %w", err)
} }
default: default:
panic("unknown upstream type: " + settings.UpstreamType) panic("unknown upstream type: " + userSettings.UpstreamType)
} }
serverSettings.Dialer = dialer serverSettings.Dialer = dialer
if *settings.Caching { if *userSettings.Caching {
lruCache, err := lru.New(lru.Settings{}) lruCache, err := lru.New(lru.Settings{})
if err != nil { if err != nil {
return server.Settings{}, fmt.Errorf("creating LRU cache: %w", err) return server.Settings{}, fmt.Errorf("creating LRU cache: %w", err)
@@ -123,3 +115,48 @@ func buildServerSettings(settings settings.DNS,
return serverSettings, nil return serverSettings, nil
} }
func buildProviders(userSettings settings.DNS) []provider.Provider {
if userSettings.UpstreamType == settings.DNSUpstreamTypePlain &&
len(userSettings.UpstreamPlainAddresses) > 0 {
providers := make([]provider.Provider, len(userSettings.UpstreamPlainAddresses))
for i, addrPort := range userSettings.UpstreamPlainAddresses {
providers[i] = provider.Provider{
Name: addrPort.String(),
}
if addrPort.Addr().Is4() {
providers[i].Plain.IPv4 = []netip.AddrPort{addrPort}
} else {
providers[i].Plain.IPv6 = []netip.AddrPort{addrPort}
}
}
}
providersData := provider.NewProviders()
providers := make([]provider.Provider, 0, len(userSettings.Providers)+len(userSettings.UpstreamPlainAddresses))
for _, providerName := range userSettings.Providers {
provider, err := providersData.Get(providerName)
if err != nil {
panic(err) // this should already had been checked
}
providers = append(providers, provider)
}
if userSettings.UpstreamType != settings.DNSUpstreamTypePlain {
return providers
}
for _, addrPort := range userSettings.UpstreamPlainAddresses {
newProvider := provider.Provider{
Name: addrPort.String(),
}
if addrPort.Addr().Is4() {
newProvider.Plain.IPv4 = []netip.AddrPort{addrPort}
} else {
newProvider.Plain.IPv6 = []netip.AddrPort{addrPort}
}
providers = append(providers, newProvider)
}
return providers
}

View File

@@ -4,7 +4,6 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"net/netip"
"github.com/qdm12/dns/v2/pkg/check" "github.com/qdm12/dns/v2/pkg/check"
"github.com/qdm12/dns/v2/pkg/nameserver" "github.com/qdm12/dns/v2/pkg/nameserver"
@@ -38,12 +37,8 @@ func (l *Loop) setupServer(ctx context.Context) (runError <-chan error, err erro
l.server = server l.server = server
// use internal DNS server // use internal DNS server
const defaultDNSPort = 53 nameserver.UseDNSInternally(nameserver.SettingsInternalDNS{})
nameserver.UseDNSInternally(nameserver.SettingsInternalDNS{
AddrPort: netip.AddrPortFrom(settings.ServerAddress, defaultDNSPort),
})
err = nameserver.UseDNSSystemWide(nameserver.SettingsSystemDNS{ err = nameserver.UseDNSSystemWide(nameserver.SettingsSystemDNS{
IPs: []netip.Addr{settings.ServerAddress},
ResolvPath: l.resolvConf, ResolvPath: l.resolvConf,
}) })
if err != nil { if err != nil {

View File

@@ -40,8 +40,6 @@ func (s *State) SetSettings(ctx context.Context, settings settings.DNS) (
// Restart // Restart
_, _ = s.statusApplier.ApplyStatus(ctx, constants.Stopped) _, _ = s.statusApplier.ApplyStatus(ctx, constants.Stopped)
if *settings.ServerEnabled {
outcome, _ = s.statusApplier.ApplyStatus(ctx, constants.Running) outcome, _ = s.statusApplier.ApplyStatus(ctx, constants.Running)
}
return outcome return outcome
} }

View File

@@ -131,9 +131,18 @@ func (c *Checker) smallPeriodicCheck(ctx context.Context) error {
c.configMutex.Lock() c.configMutex.Lock()
ip := c.icmpTarget ip := c.icmpTarget
c.configMutex.Unlock() c.configMutex.Unlock()
const maxTries = 3 tryTimeouts := []time.Duration{
const timeout = 10 * time.Second 5 * time.Second,
const extraTryTime = 10 * time.Second // 10s added for each subsequent retry 5 * time.Second,
5 * time.Second,
10 * time.Second,
10 * time.Second,
10 * time.Second,
15 * time.Second,
15 * time.Second,
15 * time.Second,
30 * time.Second,
}
check := func(ctx context.Context) error { check := func(ctx context.Context) error {
if c.icmpNotPermitted { if c.icmpNotPermitted {
return c.dnsClient.Check(ctx) return c.dnsClient.Check(ctx)
@@ -147,19 +156,17 @@ func (c *Checker) smallPeriodicCheck(ctx context.Context) error {
} }
return err return err
} }
return withRetries(ctx, maxTries, timeout, extraTryTime, c.logger, c.smallCheckName, check) return withRetries(ctx, tryTimeouts, c.logger, c.smallCheckName, check)
} }
func (c *Checker) fullPeriodicCheck(ctx context.Context) error { func (c *Checker) fullPeriodicCheck(ctx context.Context) error {
const maxTries = 2
// 20s timeout in case the connection is under stress // 20s timeout in case the connection is under stress
// See https://github.com/qdm12/gluetun/issues/2270 // See https://github.com/qdm12/gluetun/issues/2270
const timeout = 20 * time.Second tryTimeouts := []time.Duration{10 * time.Second, 15 * time.Second, 30 * time.Second}
const extraTryTime = 10 * time.Second // 10s added for each subsequent retry
check := func(ctx context.Context) error { check := func(ctx context.Context) error {
return tcpTLSCheck(ctx, c.dialer, c.tlsDialAddr) return tcpTLSCheck(ctx, c.dialer, c.tlsDialAddr)
} }
return withRetries(ctx, maxTries, timeout, extraTryTime, c.logger, "TCP+TLS dial", check) return withRetries(ctx, tryTimeouts, c.logger, "TCP+TLS dial", check)
} }
func tcpTLSCheck(ctx context.Context, dialer *net.Dialer, targetAddress string) error { func tcpTLSCheck(ctx context.Context, dialer *net.Dialer, targetAddress string) error {
@@ -218,13 +225,17 @@ func makeAddressToDial(address string) (addressToDial string, err error) {
var ErrAllCheckTriesFailed = errors.New("all check tries failed") var ErrAllCheckTriesFailed = errors.New("all check tries failed")
func withRetries(ctx context.Context, maxTries uint, tryTimeout, extraTryTime time.Duration, func withRetries(ctx context.Context, tryTimeouts []time.Duration,
logger Logger, checkName string, check func(ctx context.Context) error, logger Logger, checkName string, check func(ctx context.Context) error,
) error { ) error {
try := uint(0) maxTries := len(tryTimeouts)
var errs []error type errData struct {
for { err error
timeout := tryTimeout + time.Duration(try)*extraTryTime //nolint:gosec duration time.Duration
}
errs := make([]errData, maxTries)
for i, timeout := range tryTimeouts {
start := time.Now()
checkCtx, cancel := context.WithTimeout(ctx, timeout) checkCtx, cancel := context.WithTimeout(ctx, timeout)
err := check(checkCtx) err := check(checkCtx)
cancel() cancel()
@@ -234,17 +245,14 @@ func withRetries(ctx context.Context, maxTries uint, tryTimeout, extraTryTime ti
case ctx.Err() != nil: case ctx.Err() != nil:
return fmt.Errorf("%s: %w", checkName, ctx.Err()) return fmt.Errorf("%s: %w", checkName, ctx.Err())
} }
logger.Debugf("%s attempt %d/%d failed: %s", checkName, try+1, maxTries, err) logger.Debugf("%s attempt %d/%d failed: %s", checkName, i+1, maxTries, err)
errs = append(errs, err) errs[i].err = err
try++ errs[i].duration = time.Since(start)
if try < maxTries {
continue
} }
errStrings := make([]string, len(errs)) errStrings := make([]string, len(errs))
for i, err := range errs { for i, err := range errs {
errStrings[i] = fmt.Sprintf("attempt %d: %s", i+1, err.Error()) errStrings[i] = fmt.Sprintf("attempt %d (%s): %s", i+1, err.duration, err.err)
}
return fmt.Errorf("%w: after %d %s attempts (%s)",
ErrAllCheckTriesFailed, maxTries, checkName, strings.Join(errStrings, "; "))
} }
return fmt.Errorf("%w: %s", ErrAllCheckTriesFailed, strings.Join(errStrings, ", "))
} }

View File

@@ -18,6 +18,7 @@ func runCommand(ctx context.Context, cmder Cmder, logger Logger,
} }
portsString := strings.Join(portStrings, ",") portsString := strings.Join(portStrings, ",")
commandString := strings.ReplaceAll(commandTemplate, "{{PORTS}}", portsString) commandString := strings.ReplaceAll(commandTemplate, "{{PORTS}}", portsString)
commandString = strings.ReplaceAll(commandString, "{{PORT}}", portStrings[0])
args, err := command.Split(commandString) args, err := command.Split(commandString)
if err != nil { if err != nil {
return fmt.Errorf("parsing command: %w", err) return fmt.Errorf("parsing command: %w", err)

View File

@@ -371,7 +371,7 @@ func (c *apiClient) authInfo(ctx context.Context, username string, unauthCookie
case info.SRPSession == "": case info.SRPSession == "":
return "", "", "", "", 0, fmt.Errorf("%w: SRP session is empty", ErrDataFieldMissing) return "", "", "", "", 0, fmt.Errorf("%w: SRP session is empty", ErrDataFieldMissing)
case info.Username != username: case !strings.EqualFold(info.Username, username):
return "", "", "", "", 0, fmt.Errorf("%w: expected %s got %s", return "", "", "", "", 0, fmt.Errorf("%w: expected %s got %s",
ErrUsernameMismatch, username, info.Username) ErrUsernameMismatch, username, info.Username)
case info.Version == nil: case info.Version == nil:

View File

@@ -1,3 +1,3 @@
package storage package storage
//go:generate mockgen -destination=mocks_test.go -package $GOPACKAGE . Infoer //go:generate mockgen -destination=mocks_test.go -package $GOPACKAGE . Logger

View File

@@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT. // Code generated by MockGen. DO NOT EDIT.
// Source: github.com/qdm12/gluetun/internal/storage (interfaces: Infoer) // Source: github.com/qdm12/gluetun/internal/storage (interfaces: Logger)
// Package storage is a generated GoMock package. // Package storage is a generated GoMock package.
package storage package storage
@@ -10,37 +10,49 @@ import (
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
) )
// MockInfoer is a mock of Infoer interface. // MockLogger is a mock of Logger interface.
type MockInfoer struct { type MockLogger struct {
ctrl *gomock.Controller ctrl *gomock.Controller
recorder *MockInfoerMockRecorder recorder *MockLoggerMockRecorder
} }
// MockInfoerMockRecorder is the mock recorder for MockInfoer. // MockLoggerMockRecorder is the mock recorder for MockLogger.
type MockInfoerMockRecorder struct { type MockLoggerMockRecorder struct {
mock *MockInfoer mock *MockLogger
} }
// NewMockInfoer creates a new mock instance. // NewMockLogger creates a new mock instance.
func NewMockInfoer(ctrl *gomock.Controller) *MockInfoer { func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockInfoer{ctrl: ctrl} mock := &MockLogger{ctrl: ctrl}
mock.recorder = &MockInfoerMockRecorder{mock} mock.recorder = &MockLoggerMockRecorder{mock}
return mock return mock
} }
// EXPECT returns an object that allows the caller to indicate expected use. // EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockInfoer) EXPECT() *MockInfoerMockRecorder { func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
return m.recorder return m.recorder
} }
// Info mocks base method. // Info mocks base method.
func (m *MockInfoer) Info(arg0 string) { func (m *MockLogger) Info(arg0 string) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
m.ctrl.Call(m, "Info", arg0) m.ctrl.Call(m, "Info", arg0)
} }
// Info indicates an expected call of Info. // Info indicates an expected call of Info.
func (mr *MockInfoerMockRecorder) Info(arg0 interface{}) *gomock.Call { func (mr *MockLoggerMockRecorder) Info(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockInfoer)(nil).Info), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), arg0)
}
// Warn mocks base method.
func (m *MockLogger) Warn(arg0 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Warn", arg0)
}
// Warn indicates an expected call of Warn.
func (mr *MockLoggerMockRecorder) Warn(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockLogger)(nil).Warn), arg0)
} }

View File

@@ -95,7 +95,7 @@ func Test_extractServersFromBytes(t *testing.T) {
t.Parallel() t.Parallel()
ctrl := gomock.NewController(t) ctrl := gomock.NewController(t)
logger := NewMockInfoer(ctrl) logger := NewMockLogger(ctrl)
var previousLogCall *gomock.Call var previousLogCall *gomock.Call
for _, logged := range testCase.logged { for _, logged := range testCase.logged {
call := logger.EXPECT().Info(logged) call := logger.EXPECT().Info(logged)

View File

@@ -13,31 +13,36 @@ type Storage struct {
// the embedded JSON file on every call to the // the embedded JSON file on every call to the
// SyncServers method. // SyncServers method.
hardcodedServers models.AllServers hardcodedServers models.AllServers
logger Infoer logger Logger
filepath string filepath string
} }
type Infoer interface { type Logger interface {
Info(s string) Info(s string)
Warn(s string)
} }
// New creates a new storage and reads the servers from the // New creates a new storage and reads the servers from the
// embedded servers file and the file on disk. // embedded servers file and the file on disk.
// Passing an empty filepath disables writing servers to a file. // Passing an empty filepath disables the reading and writing of
func New(logger Infoer, filepath string) (storage *Storage, err error) { // servers.
func New(logger Logger, filepath string) (storage *Storage, err error) {
// A unit test prevents any error from being returned // A unit test prevents any error from being returned
// and ensures all providers are part of the servers returned. // and ensures all providers are part of the servers returned.
hardcodedServers, _ := parseHardcodedServers() hardcodedServers, _ := parseHardcodedServers()
storage = &Storage{ storage = &Storage{
hardcodedServers: hardcodedServers, hardcodedServers: hardcodedServers,
mergedServers: hardcodedServers,
logger: logger, logger: logger,
filepath: filepath, filepath: filepath,
} }
if filepath != "" {
if err := storage.syncServers(); err != nil { if err := storage.syncServers(); err != nil {
return nil, err return nil, err
} }
}
return storage, nil return storage, nil
} }

View File

@@ -46,13 +46,13 @@ func (s *Storage) syncServers() (err error) {
} }
// Eventually write file // Eventually write file
if s.filepath == "" || reflect.DeepEqual(serversOnFile, s.mergedServers) { if reflect.DeepEqual(serversOnFile, s.mergedServers) {
return nil return nil
} }
err = s.flushToFile(s.filepath) err = s.flushToFile(s.filepath)
if err != nil { if err != nil {
return fmt.Errorf("writing servers to file: %w", err) s.logger.Warn("failed writing servers to file: " + err.Error())
} }
return nil return nil
} }

View File

@@ -4,7 +4,6 @@ import (
"context" "context"
"net/netip" "net/netip"
"github.com/qdm12/dns/v2/pkg/check"
"github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/version" "github.com/qdm12/gluetun/internal/version"
) )
@@ -46,14 +45,7 @@ func (l *Loop) onTunnelUp(ctx, loopCtx context.Context, data tunnelUpData) {
return return
} }
if *l.dnsLooper.GetSettings().ServerEnabled {
_, _ = l.dnsLooper.ApplyStatus(ctx, constants.Running) _, _ = l.dnsLooper.ApplyStatus(ctx, constants.Running)
} else {
err := check.WaitForDNS(ctx, check.Settings{})
if err != nil {
l.logger.Error("waiting for DNS to be ready: " + err.Error())
}
}
err = l.publicip.RunOnce(ctx) err = l.publicip.RunOnce(ctx)
if err != nil { if err != nil {