From 7dbd14df27bb31bb90f4c625194c11f6c2b20be6 Mon Sep 17 00:00:00 2001 From: Quentin McGaw Date: Wed, 5 Nov 2025 20:14:25 +0000 Subject: [PATCH] chore(dns): merge DoT settings with DNS settings --- Dockerfile | 2 +- internal/configuration/settings/dns.go | 145 +++++++++++++-- internal/configuration/settings/dot.go | 170 ------------------ .../configuration/settings/settings_test.go | 21 ++- internal/dns/plaintext.go | 11 +- internal/dns/run.go | 4 +- internal/dns/settings.go | 10 +- internal/dns/state/settings.go | 4 +- internal/dns/ticker.go | 6 +- internal/dns/update.go | 2 +- internal/vpn/tunnelup.go | 2 +- 11 files changed, 155 insertions(+), 222 deletions(-) delete mode 100644 internal/configuration/settings/dot.go diff --git a/Dockerfile b/Dockerfile index c15d7bfa..20bdfd9b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -166,7 +166,7 @@ ENV VPN_SERVICE_PROVIDER=pia \ HEALTH_TARGET_ADDRESS=cloudflare.com:443 \ HEALTH_ICMP_TARGET_IP=1.1.1.1 \ HEALTH_RESTART_VPN=on \ - # DNS over TLS + # DNS DOT=on \ DOT_PROVIDERS=cloudflare \ DOT_PRIVATE_ADDRESS=127.0.0.1/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1/128,fc00::/7,fe80::/10,::ffff:7f00:1/104,::ffff:a00:0/104,::ffff:a9fe:0/112,::ffff:ac10:0/108,::ffff:c0a8:0/112 \ diff --git a/internal/configuration/settings/dns.go b/internal/configuration/settings/dns.go index 2d0988cb..9d6e0ca9 100644 --- a/internal/configuration/settings/dns.go +++ b/internal/configuration/settings/dns.go @@ -1,9 +1,12 @@ package settings import ( + "errors" "fmt" "net/netip" + "time" + "github.com/qdm12/dns/v2/pkg/provider" "github.com/qdm12/gosettings" "github.com/qdm12/gosettings/reader" "github.com/qdm12/gotree" @@ -11,6 +14,25 @@ import ( // DNS contains settings to configure DNS. type DNS struct { + // DoTEnabled is true if the DoT server should be running + // and used. It defaults to true, and cannot be nil + // in the internal state. + DoTEnabled *bool + // 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 over TLS providers + Providers []string `json:"providers"` + // Caching is true if the DoT server should cache + // DNS responses. + Caching *bool `json:"caching"` + // IPv6 is true if the DoT server should connect over IPv6. + IPv6 *bool `json:"ipv6"` + // 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 @@ -28,15 +50,28 @@ type DNS struct { // It defaults to false and cannot be nil in the // internal state. KeepNameserver *bool - // DOT contains settings to configure the DoT - // server. - DoT DoT } +var ErrDoTUpdatePeriodTooShort = errors.New("update period is too short") + func (d DNS) validate() (err error) { - err = d.DoT.validate() + const minUpdatePeriod = 30 * time.Second + if *d.UpdatePeriod != 0 && *d.UpdatePeriod < minUpdatePeriod { + return fmt.Errorf("%w: %s must be bigger than %s", + ErrDoTUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod) + } + + providers := provider.NewProviders() + for _, providerName := range d.Providers { + _, err := providers.Get(providerName) + if err != nil { + return err + } + } + + err = d.Blacklist.validate() if err != nil { - return fmt.Errorf("validating DoT settings: %w", err) + return err } return nil @@ -44,9 +79,14 @@ func (d DNS) validate() (err error) { func (d *DNS) Copy() (copied DNS) { return DNS{ + DoTEnabled: gosettings.CopyPointer(d.DoTEnabled), + 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), - DoT: d.DoT.copy(), } } @@ -54,16 +94,46 @@ func (d *DNS) Copy() (copied DNS) { // settings object with any field set in the other // settings. func (d *DNS) overrideWith(other DNS) { + d.DoTEnabled = gosettings.OverrideWithPointer(d.DoTEnabled, other.DoTEnabled) + d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod) + d.Providers = gosettings.OverrideWithSlice(d.Providers, other.Providers) + 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.DoT.overrideWith(other.DoT) } func (d *DNS) setDefaults() { - localhost := netip.AddrFrom4([4]byte{127, 0, 0, 1}) - d.ServerAddress = gosettings.DefaultValidator(d.ServerAddress, localhost) + d.DoTEnabled = gosettings.DefaultPointer(d.DoTEnabled, true) + const defaultUpdatePeriod = 24 * time.Hour + d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod) + d.Providers = gosettings.DefaultSlice(d.Providers, []string{ + provider.Cloudflare().Name, + }) + 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) - d.DoT.setDefaults() +} + +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 + } + + 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 { @@ -77,11 +147,59 @@ func (d DNS) toLinesNode() (node *gotree.Node) { return node } node.Appendf("DNS server address to use: %s", d.ServerAddress) - node.AppendNode(d.DoT.toLinesNode()) + + node.Appendf("DNS over TLS forwarder enabled: %s", gosettings.BoolToYesNo(d.DoTEnabled)) + if !*d.DoTEnabled { + return node + } + + update := "disabled" + if *d.UpdatePeriod > 0 { + update = "every " + d.UpdatePeriod.String() + } + node.Appendf("Update period: %s", update) + + upstreamResolvers := node.Append("Upstream resolvers:") + for _, provider := range d.Providers { + upstreamResolvers.Append(provider) + } + + node.Appendf("Caching: %s", gosettings.BoolToYesNo(d.Caching)) + node.Appendf("IPv6: %s", gosettings.BoolToYesNo(d.IPv6)) + + node.AppendNode(d.Blacklist.toLinesNode()) + return node } func (d *DNS) read(r *reader.Reader) (err error) { + d.DoTEnabled, err = r.BoolPtr("DOT") + if err != nil { + return err + } + + d.UpdatePeriod, err = r.DurationPtr("DNS_UPDATE_PERIOD") + if err != nil { + return err + } + + d.Providers = r.CSV("DOT_PROVIDERS") + + d.Caching, err = r.BoolPtr("DOT_CACHING") + if err != nil { + return err + } + + d.IPv6, err = r.BoolPtr("DOT_IPV6") + if err != nil { + return err + } + + err = d.Blacklist.read(r) + if err != nil { + return err + } + d.ServerAddress, err = r.NetipAddr("DNS_ADDRESS", reader.RetroKeys("DNS_PLAINTEXT_ADDRESS")) if err != nil { return err @@ -92,10 +210,5 @@ func (d *DNS) read(r *reader.Reader) (err error) { return err } - err = d.DoT.read(r) - if err != nil { - return fmt.Errorf("DNS over TLS settings: %w", err) - } - return nil } diff --git a/internal/configuration/settings/dot.go b/internal/configuration/settings/dot.go deleted file mode 100644 index 0b6478d0..00000000 --- a/internal/configuration/settings/dot.go +++ /dev/null @@ -1,170 +0,0 @@ -package settings - -import ( - "errors" - "fmt" - "net/netip" - "time" - - "github.com/qdm12/dns/v2/pkg/provider" - "github.com/qdm12/gosettings" - "github.com/qdm12/gosettings/reader" - "github.com/qdm12/gotree" -) - -// DoT contains settings to configure the DoT server. -type DoT struct { - // Enabled is true if the DoT server should be running - // and used. It defaults to true, and cannot be nil - // in the internal state. - Enabled *bool - // 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 over TLS providers - Providers []string `json:"providers"` - // Caching is true if the DoT server should cache - // DNS responses. - Caching *bool `json:"caching"` - // IPv6 is true if the DoT server should connect over IPv6. - IPv6 *bool `json:"ipv6"` - // Blacklist contains settings to configure the filter - // block lists. - Blacklist DNSBlacklist -} - -var ErrDoTUpdatePeriodTooShort = errors.New("update period is too short") - -func (d DoT) validate() (err error) { - const minUpdatePeriod = 30 * time.Second - if *d.UpdatePeriod != 0 && *d.UpdatePeriod < minUpdatePeriod { - return fmt.Errorf("%w: %s must be bigger than %s", - ErrDoTUpdatePeriodTooShort, *d.UpdatePeriod, minUpdatePeriod) - } - - providers := provider.NewProviders() - for _, providerName := range d.Providers { - _, err := providers.Get(providerName) - if err != nil { - return err - } - } - - err = d.Blacklist.validate() - if err != nil { - return err - } - - return nil -} - -func (d *DoT) copy() (copied DoT) { - return DoT{ - Enabled: gosettings.CopyPointer(d.Enabled), - UpdatePeriod: gosettings.CopyPointer(d.UpdatePeriod), - Providers: gosettings.CopySlice(d.Providers), - Caching: gosettings.CopyPointer(d.Caching), - IPv6: gosettings.CopyPointer(d.IPv6), - Blacklist: d.Blacklist.copy(), - } -} - -// overrideWith overrides fields of the receiver -// settings object with any field set in the other -// settings. -func (d *DoT) overrideWith(other DoT) { - d.Enabled = gosettings.OverrideWithPointer(d.Enabled, other.Enabled) - d.UpdatePeriod = gosettings.OverrideWithPointer(d.UpdatePeriod, other.UpdatePeriod) - d.Providers = gosettings.OverrideWithSlice(d.Providers, other.Providers) - d.Caching = gosettings.OverrideWithPointer(d.Caching, other.Caching) - d.IPv6 = gosettings.OverrideWithPointer(d.IPv6, other.IPv6) - d.Blacklist.overrideWith(other.Blacklist) -} - -func (d *DoT) setDefaults() { - d.Enabled = gosettings.DefaultPointer(d.Enabled, true) - const defaultUpdatePeriod = 24 * time.Hour - d.UpdatePeriod = gosettings.DefaultPointer(d.UpdatePeriod, defaultUpdatePeriod) - d.Providers = gosettings.DefaultSlice(d.Providers, []string{ - provider.Cloudflare().Name, - }) - d.Caching = gosettings.DefaultPointer(d.Caching, true) - d.IPv6 = gosettings.DefaultPointer(d.IPv6, false) - d.Blacklist.setDefaults() -} - -func (d DoT) GetFirstPlaintextIPv4() (ipv4 netip.Addr) { - 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.DoT.IPv4[0].Addr() -} - -func (d DoT) String() string { - return d.toLinesNode().String() -} - -func (d DoT) toLinesNode() (node *gotree.Node) { - node = gotree.New("DNS over TLS settings:") - - node.Appendf("Enabled: %s", gosettings.BoolToYesNo(d.Enabled)) - if !*d.Enabled { - return node - } - - update := "disabled" - if *d.UpdatePeriod > 0 { - update = "every " + d.UpdatePeriod.String() - } - node.Appendf("Update period: %s", update) - - upstreamResolvers := node.Append("Upstream resolvers:") - for _, provider := range d.Providers { - upstreamResolvers.Append(provider) - } - - node.Appendf("Caching: %s", gosettings.BoolToYesNo(d.Caching)) - node.Appendf("IPv6: %s", gosettings.BoolToYesNo(d.IPv6)) - - node.AppendNode(d.Blacklist.toLinesNode()) - - return node -} - -func (d *DoT) read(reader *reader.Reader) (err error) { - d.Enabled, err = reader.BoolPtr("DOT") - if err != nil { - return err - } - - d.UpdatePeriod, err = reader.DurationPtr("DNS_UPDATE_PERIOD") - if err != nil { - return err - } - - d.Providers = reader.CSV("DOT_PROVIDERS") - - d.Caching, err = reader.BoolPtr("DOT_CACHING") - if err != nil { - return err - } - - d.IPv6, err = reader.BoolPtr("DOT_IPV6") - if err != nil { - return err - } - - err = d.Blacklist.read(reader) - if err != nil { - return err - } - - return nil -} diff --git a/internal/configuration/settings/settings_test.go b/internal/configuration/settings/settings_test.go index 26caf4f9..f97de8b2 100644 --- a/internal/configuration/settings/settings_test.go +++ b/internal/configuration/settings/settings_test.go @@ -40,17 +40,16 @@ func Test_Settings_String(t *testing.T) { ├── DNS settings: | ├── Keep existing nameserver(s): no | ├── DNS server address to use: 127.0.0.1 -| └── DNS over TLS settings: -| ├── Enabled: yes -| ├── Update period: every 24h0m0s -| ├── Upstream resolvers: -| | └── Cloudflare -| ├── Caching: yes -| ├── IPv6: no -| └── DNS filtering settings: -| ├── Block malicious: yes -| ├── Block ads: no -| └── Block surveillance: yes +| ├── DNS over TLS forwarder enabled: yes +| ├── Update period: every 24h0m0s +| ├── Upstream resolvers: +| | └── Cloudflare +| ├── Caching: yes +| ├── IPv6: no +| └── DNS filtering settings: +| ├── Block malicious: yes +| ├── Block ads: no +| └── Block surveillance: yes ├── Firewall settings: | └── Enabled: yes ├── Log settings: diff --git a/internal/dns/plaintext.go b/internal/dns/plaintext.go index d005c06f..62ab8ac3 100644 --- a/internal/dns/plaintext.go +++ b/internal/dns/plaintext.go @@ -1,7 +1,6 @@ package dns import ( - "net/netip" "time" "github.com/qdm12/dns/v2/pkg/nameserver" @@ -10,15 +9,7 @@ import ( func (l *Loop) useUnencryptedDNS(fallback bool) { settings := l.GetSettings() - // Try with user provided plaintext ip address - // if it's not 127.0.0.1 (default for DoT), otherwise - // use the first DoT provider ipv4 address found. - var targetIP netip.Addr - if settings.ServerAddress.Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) != 0 { - targetIP = settings.ServerAddress - } else { - targetIP = settings.DoT.GetFirstPlaintextIPv4() - } + targetIP := settings.GetFirstPlaintextIPv4() if fallback { l.logger.Info("falling back on plaintext DNS at address " + targetIP.String()) diff --git a/internal/dns/run.go b/internal/dns/run.go index f5d05e44..f56474a2 100644 --- a/internal/dns/run.go +++ b/internal/dns/run.go @@ -31,7 +31,7 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) { var runError <-chan error settings := l.GetSettings() - for !*settings.KeepNameserver && *settings.DoT.Enabled { + for !*settings.KeepNameserver && *settings.DoTEnabled { var err error runError, err = l.setupServer(ctx) if err == nil { @@ -56,7 +56,7 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) { } settings = l.GetSettings() - if !*settings.KeepNameserver && !*settings.DoT.Enabled { + if !*settings.KeepNameserver && !*settings.DoTEnabled { const fallback = false l.useUnencryptedDNS(fallback) } diff --git a/internal/dns/settings.go b/internal/dns/settings.go index bfad5e12..aaff6d67 100644 --- a/internal/dns/settings.go +++ b/internal/dns/settings.go @@ -30,16 +30,16 @@ func buildDoTSettings(settings settings.DNS, var dotSettings dot.Settings providersData := provider.NewProviders() - dotSettings.UpstreamResolvers = make([]provider.Provider, len(settings.DoT.Providers)) - for i := range settings.DoT.Providers { + dotSettings.UpstreamResolvers = make([]provider.Provider, len(settings.Providers)) + for i := range settings.Providers { var err error - dotSettings.UpstreamResolvers[i], err = providersData.Get(settings.DoT.Providers[i]) + dotSettings.UpstreamResolvers[i], err = providersData.Get(settings.Providers[i]) if err != nil { panic(err) // this should already had been checked } } dotSettings.IPVersion = "ipv4" - if *settings.DoT.IPv6 { + if *settings.IPv6 { dotSettings.IPVersion = "ipv6" } @@ -48,7 +48,7 @@ func buildDoTSettings(settings settings.DNS, return server.Settings{}, fmt.Errorf("creating DNS over TLS dialer: %w", err) } - if *settings.DoT.Caching { + if *settings.Caching { lruCache, err := lru.New(lru.Settings{}) if err != nil { return server.Settings{}, fmt.Errorf("creating LRU cache: %w", err) diff --git a/internal/dns/state/settings.go b/internal/dns/state/settings.go index 4384e905..a6cce5d4 100644 --- a/internal/dns/state/settings.go +++ b/internal/dns/state/settings.go @@ -27,7 +27,7 @@ func (s *State) SetSettings(ctx context.Context, settings settings.DNS) ( // Check for only update period change tempSettings := s.settings.Copy() - *tempSettings.DoT.UpdatePeriod = *settings.DoT.UpdatePeriod + *tempSettings.UpdatePeriod = *settings.UpdatePeriod onlyUpdatePeriodChanged := reflect.DeepEqual(tempSettings, settings) s.settings = settings @@ -40,7 +40,7 @@ func (s *State) SetSettings(ctx context.Context, settings settings.DNS) ( // Restart _, _ = s.statusApplier.ApplyStatus(ctx, constants.Stopped) - if *settings.DoT.Enabled { + if *settings.DoTEnabled { outcome, _ = s.statusApplier.ApplyStatus(ctx, constants.Running) } return outcome diff --git a/internal/dns/ticker.go b/internal/dns/ticker.go index f926bc49..f2f3c146 100644 --- a/internal/dns/ticker.go +++ b/internal/dns/ticker.go @@ -14,7 +14,7 @@ func (l *Loop) RunRestartTicker(ctx context.Context, done chan<- struct{}) { timer.Stop() timerIsStopped := true settings := l.GetSettings() - if period := *settings.DoT.UpdatePeriod; period > 0 { + if period := *settings.UpdatePeriod; period > 0 { timer.Reset(period) timerIsStopped = false } @@ -43,14 +43,14 @@ func (l *Loop) RunRestartTicker(ctx context.Context, done chan<- struct{}) { _, _ = l.statusManager.ApplyStatus(ctx, constants.Running) settings := l.GetSettings() - timer.Reset(*settings.DoT.UpdatePeriod) + timer.Reset(*settings.UpdatePeriod) case <-l.updateTicker: if !timer.Stop() { <-timer.C } timerIsStopped = true settings := l.GetSettings() - newUpdatePeriod := *settings.DoT.UpdatePeriod + newUpdatePeriod := *settings.UpdatePeriod if newUpdatePeriod == 0 { continue } diff --git a/internal/dns/update.go b/internal/dns/update.go index 9526d53b..4a72876d 100644 --- a/internal/dns/update.go +++ b/internal/dns/update.go @@ -12,7 +12,7 @@ func (l *Loop) updateFiles(ctx context.Context) (err error) { settings := l.GetSettings() l.logger.Info("downloading hostnames and IP block lists") - blacklistSettings := settings.DoT.Blacklist.ToBlockBuilderSettings(l.client) + blacklistSettings := settings.Blacklist.ToBlockBuilderSettings(l.client) blockBuilder, err := blockbuilder.New(blacklistSettings) if err != nil { diff --git a/internal/vpn/tunnelup.go b/internal/vpn/tunnelup.go index 4ec8086a..3e02295c 100644 --- a/internal/vpn/tunnelup.go +++ b/internal/vpn/tunnelup.go @@ -46,7 +46,7 @@ func (l *Loop) onTunnelUp(ctx, loopCtx context.Context, data tunnelUpData) { return } - if *l.dnsLooper.GetSettings().DoT.Enabled { + if *l.dnsLooper.GetSettings().DoTEnabled { _, _ = l.dnsLooper.ApplyStatus(ctx, constants.Running) } else { err := check.WaitForDNS(ctx, check.Settings{})