chore(dns): remove DNS_SERVER, DNS_KEEP_NAMESERVER and replace DNS_ADDRESS with DNS_UPSTREAM_PLAIN_ADDRESSES (#2988)

- Remove `DNS_SERVER` (aka DOT) option: the DNS server forwarder part is now always enabled (see below why)
- Remove `DNS_KEEP_NAMESERVER`: the container will always use the built-in DNS server forwarder, because it can handle now local names with local resolvers (see #2970), it can use the `plain` upstream type (see https://github.com/qdm12/gluetun/commit/5ed6e8292278b54bb5081de0e8ccd0d63a275b3c) AND you can use `DNS_UPSTREAM_PLAIN_ADDRESSES` (see below)
- Replace `DNS_ADDRESS` with `DNS_UPSTREAM_PLAIN_ADDRESSES`:
  - New CSV format with port, for example `ip1:port1,ip2:port2`
  - requires `DNS_UPSTREAM_TYPE=plain` to be set to use `DNS_UPSTREAM_PLAIN_ADDRESSES` (unless using retro `DNS_ADDRESS`)
  - 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.
  - Warning log on using private upstream resolvers updated
- Warning log if using a private IP address for the plain DNS server which is not in your local subnets
All in all, this greatly simplifies code and available options (less options for the same features is a win). It also allows you to specify multiple plain DNS resolvers on ports other than 53 if needed.
This commit is contained in:
Quentin McGaw
2026-03-07 14:07:57 +01:00
committed by GitHub
parent dd0edafbb1
commit 199ad77ec9
16 changed files with 252 additions and 162 deletions
+1 -3
View File
@@ -175,7 +175,6 @@ ENV VPN_SERVICE_PROVIDER=pia \
HEALTH_SMALL_CHECK_TYPE=icmp \ HEALTH_SMALL_CHECK_TYPE=icmp \
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= \
@@ -188,8 +187,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 \
+9 -1
View File
@@ -402,7 +402,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
dnsLogger := logger.New(log.SetComponent("dns")) dnsLogger := logger.New(log.SetComponent("dns"))
dnsLooper, err := dns.NewLoop(allSettings.DNS, httpClient, dnsLooper, err := dns.NewLoop(allSettings.DNS, httpClient,
dnsLogger) dnsLogger, localNetworksToPrefixes(localNetworks))
if err != nil { if err != nil {
return fmt.Errorf("creating DNS loop: %w", err) return fmt.Errorf("creating DNS loop: %w", err)
} }
@@ -567,6 +567,14 @@ func printVersions(ctx context.Context, logger infoer,
return nil return nil
} }
func localNetworksToPrefixes(localNetworks []routing.LocalNetwork) (prefixes []netip.Prefix) {
prefixes = make([]netip.Prefix, len(localNetworks))
for i, localNetwork := range localNetworks {
prefixes[i] = localNetwork.IPNet
}
return prefixes
}
type netLinker interface { type netLinker interface {
Addresser Addresser
Router Router
+1 -1
View File
@@ -13,7 +13,7 @@ require (
github.com/mdlayher/genetlink v1.3.2 github.com/mdlayher/genetlink v1.3.2
github.com/mdlayher/netlink v1.7.2 github.com/mdlayher/netlink v1.7.2
github.com/pelletier/go-toml/v2 v2.2.4 github.com/pelletier/go-toml/v2 v2.2.4
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260216151239-36b3306f2205 github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260227125914-6dbc4b280dc6
github.com/qdm12/gosettings v0.4.4 github.com/qdm12/gosettings v0.4.4
github.com/qdm12/goshutdown v0.3.0 github.com/qdm12/goshutdown v0.3.0
github.com/qdm12/gosplash v0.2.1-0.20260305164749-b713de4fee6c github.com/qdm12/gosplash v0.2.1-0.20260305164749-b713de4fee6c
+2 -2
View File
@@ -74,8 +74,8 @@ github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPA
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260216151239-36b3306f2205 h1:0ycKUDQ50cYb2QpeyGcEnvVs9HJmC9jsb/XZNC1z28c= github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260227125914-6dbc4b280dc6 h1:N0UtsWF8NS1YuQ18Ws3hrhRAPmngoCns4KxoQjAcwaw=
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260216151239-36b3306f2205/go.mod h1:98foWgXJZ+g8gJIuO+fdO+oWpFei5WShMFTeN4Im2lE= github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260227125914-6dbc4b280dc6/go.mod h1:98foWgXJZ+g8gJIuO+fdO+oWpFei5WShMFTeN4Im2lE=
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 h1:TRGpCU1l0lNwtogEUSs5U+RFceYxkAJUmrGabno7J5c= github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 h1:TRGpCU1l0lNwtogEUSs5U+RFceYxkAJUmrGabno7J5c=
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978/go.mod h1:D1Po4CRQLYjccnAR2JsVlN1sBMgQrcNLONbvyuzcdTg= github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978/go.mod h1:D1Po4CRQLYjccnAR2JsVlN1sBMgQrcNLONbvyuzcdTg=
github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4= github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4=
@@ -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)
+128 -74
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(), UpstreamPlainAddresses: d.UpstreamPlainAddresses,
ServerAddress: d.ServerAddress,
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,26 +131,53 @@ 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 {
if err != nil { provider, err := providers.Get(name)
// Settings should be validated before calling this function, if err != nil {
// so an error happening here is a programming error. // Settings should be validated before calling this function,
panic(err) // so an error happening here is a programming error.
panic(err)
}
if len(provider.Plain.IPv4) > 0 {
return provider.Plain.IPv4[0].Addr()
}
} }
return netip.Addr{}
return provider.Plain.IPv4[0].Addr()
} }
func (d DNS) String() string { func (d DNS) String() string {
@@ -155,22 +186,22 @@ 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:")
for _, provider := range d.Providers { if len(d.UpstreamPlainAddresses) > 0 {
upstreamResolvers.Append(provider) 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 {
upstreamResolvers.Append(provider)
}
} }
node.Appendf("Caching: %s", gosettings.BoolToYesNo(d.Caching)) node.Appendf("Caching: %s", gosettings.BoolToYesNo(d.Caching))
@@ -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
}
@@ -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")
}
+5 -8
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"
@@ -180,13 +179,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
@@ -51,9 +51,6 @@ func Test_Settings_String(t *testing.T) {
| ├── [2606:4700:4700::1111]:443 | ├── [2606:4700:4700::1111]:443
| └── [2001:4860:4860::8888]:443 | └── [2001:4860:4860::8888]:443
├── 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
+2
View File
@@ -3,6 +3,8 @@ package dns
type Logger interface { type Logger interface {
Debug(s string) Debug(s string)
Info(s string) Info(s string)
Infof(format string, args ...any)
Warn(s string) Warn(s string)
Warnf(format string, args ...any)
Error(s string) Error(s string)
} }
+3 -1
View File
@@ -22,6 +22,7 @@ type Loop struct {
server *server.Server server *server.Server
filter *mapfilter.Filter filter *mapfilter.Filter
localResolvers []netip.Addr localResolvers []netip.Addr
localSubnets []netip.Prefix
resolvConf string resolvConf string
client *http.Client client *http.Client
logger Logger logger Logger
@@ -39,7 +40,7 @@ type Loop struct {
const defaultBackoffTime = 10 * time.Second const defaultBackoffTime = 10 * time.Second
func NewLoop(settings settings.DNS, func NewLoop(settings settings.DNS,
client *http.Client, logger Logger, client *http.Client, logger Logger, localSubnets []netip.Prefix,
) (loop *Loop, err error) { ) (loop *Loop, err error) {
start := make(chan struct{}) start := make(chan struct{})
running := make(chan models.LoopStatus) running := make(chan models.LoopStatus)
@@ -62,6 +63,7 @@ func NewLoop(settings settings.DNS,
state: state, state: state,
server: nil, server: nil,
filter: filter, filter: filter,
localSubnets: localSubnets,
resolvConf: "/etc/resolv.conf", resolvConf: "/etc/resolv.conf",
client: client, client: client,
logger: logger, logger: logger,
+11 -30
View File
@@ -17,14 +17,8 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
return return
} }
if *l.GetSettings().KeepNameserver { const fallback = false
l.logger.Warn("⚠️⚠️⚠️ keeping the default container nameservers, " + l.useUnencryptedDNS(fallback)
"this will likely leak DNS traffic outside the VPN " +
"and go through your container network DNS outside the VPN tunnel!")
} else {
const fallback = false
l.useUnencryptedDNS(fallback)
}
select { select {
case <-l.start: case <-l.start:
@@ -37,13 +31,13 @@ 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 { settings := l.GetSettings()
var err error var err error
runError, err = l.setupServer(ctx) runError, err = l.setupServer(ctx, settings)
if err == nil { if err == nil {
l.backoffTime = defaultBackoffTime l.backoffTime = defaultBackoffTime
l.logger.Info("ready and using DNS server at address " + settings.ServerAddress.String()) l.logger.Infof("ready and using DNS server with %s upstream resolvers", settings.UpstreamType)
err = l.updateFiles(ctx, settings) err = l.updateFiles(ctx, settings)
if err != nil { if err != nil {
@@ -58,16 +52,9 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
return return
} }
l.logAndWait(ctx, err) l.logAndWait(ctx, err)
settings = l.GetSettings()
} }
l.signalOrSetStatus(constants.Running) l.signalOrSetStatus(constants.Running)
settings = l.GetSettings()
if !*settings.KeepNameserver && !*settings.ServerEnabled {
const fallback = false
l.useUnencryptedDNS(fallback)
}
l.userTrigger = false l.userTrigger = false
exitLoop := l.runWait(ctx, runError) exitLoop := l.runWait(ctx, runError)
@@ -81,21 +68,15 @@ func (l *Loop) runWait(ctx context.Context, runError <-chan error) (exitLoop boo
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
settings := l.GetSettings() l.stopServer()
if !*settings.KeepNameserver && *settings.ServerEnabled { // TODO revert OS and Go nameserver when exiting
l.stopServer()
// TODO revert OS and Go nameserver when exiting
}
return true return true
case <-l.stop: case <-l.stop:
l.userTrigger = true l.userTrigger = true
l.logger.Info("stopping") l.logger.Info("stopping")
settings := l.GetSettings() const fallback = false
if !*settings.KeepNameserver && *settings.ServerEnabled { l.useUnencryptedDNS(fallback)
const fallback = false l.stopServer()
l.useUnencryptedDNS(fallback)
l.stopServer()
}
l.stopped <- struct{}{} l.stopped <- struct{}{}
case <-l.start: case <-l.start:
l.userTrigger = true l.userTrigger = true
+54 -18
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"net/netip" "net/netip"
"slices"
"github.com/qdm12/dns/v2/pkg/doh" "github.com/qdm12/dns/v2/pkg/doh"
"github.com/qdm12/dns/v2/pkg/dot" "github.com/qdm12/dns/v2/pkg/dot"
@@ -26,31 +27,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) ( localSubnets []netip.Prefix, logger Logger) (
serverSettings server.Settings, err error, serverSettings server.Settings, err error,
) { ) {
serverSettings.Logger = logger serverSettings.Logger = logger
providersData := provider.NewProviders() upstreamResolvers := buildProviders(userSettings, localSubnets, logger)
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 +52,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 +61,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 +71,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 +116,46 @@ func buildServerSettings(settings settings.DNS,
return serverSettings, nil return serverSettings, nil
} }
func buildProviders(userSettings settings.DNS, localSubnets []netip.Prefix,
logger Logger,
) (providers []provider.Provider) {
providersCount := len(userSettings.Providers)
if userSettings.UpstreamType == settings.DNSUpstreamTypePlain {
providersCount += len(userSettings.UpstreamPlainAddresses)
}
providers = make([]provider.Provider, 0, providersCount)
providersData := provider.NewProviders()
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)
}
for _, addrPort := range userSettings.UpstreamPlainAddresses {
addr := addrPort.Addr()
if addr.IsPrivate() && !addr.IsLoopback() &&
!slices.ContainsFunc(localSubnets, func(prefix netip.Prefix) bool {
return prefix.Contains(addr)
}) {
logger.Warnf("DNS server address %s is not in local subnets, "+
"make sure to specify it in FIREWALL_OUTBOUND_SUBNETS as %s",
addr, netip.PrefixFrom(addr, addr.BitLen()))
}
provider := provider.Provider{
Name: addrPort.String(),
}
if addr.Is4() {
provider.Plain.IPv4 = []netip.AddrPort{addrPort}
} else {
provider.Plain.IPv6 = []netip.AddrPort{addrPort}
}
providers = append(providers, provider)
}
return providers
}
+4 -9
View File
@@ -3,16 +3,15 @@ package dns
import ( import (
"context" "context"
"fmt" "fmt"
"net/netip"
"github.com/qdm12/dns/v2/pkg/check" "github.com/qdm12/dns/v2/pkg/check"
"github.com/qdm12/dns/v2/pkg/middlewares/filter/update" "github.com/qdm12/dns/v2/pkg/middlewares/filter/update"
"github.com/qdm12/dns/v2/pkg/nameserver" "github.com/qdm12/dns/v2/pkg/nameserver"
"github.com/qdm12/dns/v2/pkg/server" "github.com/qdm12/dns/v2/pkg/server"
"github.com/qdm12/gluetun/internal/configuration/settings"
) )
func (l *Loop) setupServer(ctx context.Context) (runError <-chan error, err error) { func (l *Loop) setupServer(ctx context.Context, settings settings.DNS) (runError <-chan error, err error) {
settings := l.GetSettings()
var updateSettings update.Settings var updateSettings update.Settings
updateSettings.SetRebindingProtectionExempt(settings.Blacklist.RebindingProtectionExemptHostnames) updateSettings.SetRebindingProtectionExempt(settings.Blacklist.RebindingProtectionExemptHostnames)
err = l.filter.Update(updateSettings) err = l.filter.Update(updateSettings)
@@ -20,7 +19,7 @@ func (l *Loop) setupServer(ctx context.Context) (runError <-chan error, err erro
return nil, fmt.Errorf("updating filter for rebinding protection: %w", err) return nil, fmt.Errorf("updating filter for rebinding protection: %w", err)
} }
serverSettings, err := buildServerSettings(settings, l.filter, l.localResolvers, l.logger) serverSettings, err := buildServerSettings(settings, l.filter, l.localResolvers, l.localSubnets, l.logger)
if err != nil { if err != nil {
return nil, fmt.Errorf("building server settings: %w", err) return nil, fmt.Errorf("building server settings: %w", err)
} }
@@ -37,12 +36,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 {
+1 -3
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
} }
+1 -9
View File
@@ -6,7 +6,6 @@ import (
"net/netip" "net/netip"
"time" "time"
"github.com/qdm12/dns/v2/pkg/check"
"github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/netlink" "github.com/qdm12/gluetun/internal/netlink"
"github.com/qdm12/gluetun/internal/pmtud" "github.com/qdm12/gluetun/internal/pmtud"
@@ -91,14 +90,7 @@ func (l *Loop) onTunnelUp(ctx, loopCtx context.Context, data tunnelUpData) {
// to start monitoring health and auto-healing. // to start monitoring health and auto-healing.
go l.collectHealthErrors(ctx, loopCtx, healthErrCh) go l.collectHealthErrors(ctx, loopCtx, healthErrCh)
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 {