From 9436f604bac7ddff3e31082c5c4260553823dcca Mon Sep 17 00:00:00 2001 From: "Quentin McGaw (desktop)" Date: Fri, 23 Jul 2021 18:55:53 +0000 Subject: [PATCH] Maint: split Go files in dns package --- internal/dns/loop.go | 269 +------------------------------------- internal/dns/plaintext.go | 36 +++++ internal/dns/run.go | 97 ++++++++++++++ internal/dns/settings.go | 19 +++ internal/dns/setup.go | 58 ++++++++ internal/dns/status.go | 19 +++ internal/dns/ticker.go | 66 ++++++++++ internal/dns/update.go | 25 ++++ 8 files changed, 322 insertions(+), 267 deletions(-) create mode 100644 internal/dns/plaintext.go create mode 100644 internal/dns/run.go create mode 100644 internal/dns/settings.go create mode 100644 internal/dns/setup.go create mode 100644 internal/dns/status.go create mode 100644 internal/dns/ticker.go create mode 100644 internal/dns/update.go diff --git a/internal/dns/loop.go b/internal/dns/loop.go index a4d4a611..a5b7132b 100644 --- a/internal/dns/loop.go +++ b/internal/dns/loop.go @@ -3,14 +3,10 @@ package dns import ( "context" - "errors" - "net" "net/http" "time" "github.com/qdm12/dns/pkg/blacklist" - "github.com/qdm12/dns/pkg/check" - "github.com/qdm12/dns/pkg/nameserver" "github.com/qdm12/dns/pkg/unbound" "github.com/qdm12/gluetun/internal/configuration" "github.com/qdm12/gluetun/internal/constants" @@ -49,7 +45,7 @@ type looper struct { const defaultBackoffTime = 10 * time.Second -func NewLooper(conf unbound.Configurator, settings configuration.DNS, client *http.Client, +func NewLoop(conf unbound.Configurator, settings configuration.DNS, client *http.Client, logger logging.Logger) Looper { start := make(chan struct{}) running := make(chan models.LoopStatus) @@ -99,270 +95,9 @@ func (l *looper) signalOrSetStatus(status models.LoopStatus) { l.userTrigger = false select { case l.running <- status: - default: // receiver droppped out - avoid deadlock on events routing when shutting down + default: // receiver dropped out - avoid deadlock on events routing when shutting down } } else { l.state.SetStatus(status) } } - -func (l *looper) Run(ctx context.Context, done chan<- struct{}) { - defer close(done) - - const fallback = false - l.useUnencryptedDNS(fallback) // TODO remove? Use default DNS by default for Docker resolution? - // TODO this one is kept if DNS_KEEP_NAMESERVER=on and should be replaced - - select { - case <-l.start: - case <-ctx.Done(): - return - } - - for ctx.Err() == nil { - // Upper scope variables for Unbound only - // Their values are to be used if DOT=off - waitError := make(chan error) - unboundCancel := func() { waitError <- nil } - closeStreams := func() {} - - for l.GetSettings().Enabled { - var err error - unboundCancel, waitError, closeStreams, err = l.setupUnbound(ctx) - if err == nil { - l.backoffTime = defaultBackoffTime - l.logger.Info("ready") - l.signalOrSetStatus(constants.Running) - break - } - - l.signalOrSetStatus(constants.Crashed) - - if ctx.Err() != nil { - return - } - - if !errors.Is(err, errUpdateFiles) { - const fallback = true - l.useUnencryptedDNS(fallback) - } - l.logAndWait(ctx, err) - } - - if !l.GetSettings().Enabled { - const fallback = false - l.useUnencryptedDNS(fallback) - } - - l.userTrigger = false - - stayHere := true - for stayHere { - select { - case <-ctx.Done(): - unboundCancel() - <-waitError - close(waitError) - closeStreams() - return - case <-l.stop: - l.userTrigger = true - l.logger.Info("stopping") - const fallback = false - l.useUnencryptedDNS(fallback) - unboundCancel() - <-waitError - // do not close waitError or the waitError - // select case will trigger - closeStreams() - l.stopped <- struct{}{} - case <-l.start: - l.userTrigger = true - l.logger.Info("starting") - stayHere = false - case err := <-waitError: // unexpected error - close(waitError) - closeStreams() - - unboundCancel() - l.state.SetStatus(constants.Crashed) - const fallback = true - l.useUnencryptedDNS(fallback) - l.logAndWait(ctx, err) - stayHere = false - } - } - } -} - -var errUpdateFiles = errors.New("cannot update files") - -// Returning cancel == nil signals we want to re-run setupUnbound -// Returning err == errUpdateFiles signals we should not fall back -// on the plaintext DNS as DOT is still up and running. -func (l *looper) setupUnbound(ctx context.Context) ( - cancel context.CancelFunc, waitError chan error, closeStreams func(), err error) { - err = l.updateFiles(ctx) - if err != nil { - return nil, nil, nil, errUpdateFiles - } - - settings := l.GetSettings() - - unboundCtx, cancel := context.WithCancel(context.Background()) - stdoutLines, stderrLines, waitError, err := l.conf.Start(unboundCtx, settings.Unbound.VerbosityDetailsLevel) - if err != nil { - cancel() - return nil, nil, nil, err - } - - collectLinesDone := make(chan struct{}) - go l.collectLines(stdoutLines, stderrLines, collectLinesDone) - closeStreams = func() { - close(stdoutLines) - close(stderrLines) - <-collectLinesDone - } - - // use Unbound - nameserver.UseDNSInternally(net.IP{127, 0, 0, 1}) - err = nameserver.UseDNSSystemWide(l.resolvConf, net.IP{127, 0, 0, 1}, - settings.KeepNameserver) - if err != nil { - l.logger.Error(err.Error()) - } - - if err := check.WaitForDNS(ctx, net.DefaultResolver); err != nil { - cancel() - <-waitError - close(waitError) - closeStreams() - return nil, nil, nil, err - } - - return cancel, waitError, closeStreams, nil -} - -func (l *looper) useUnencryptedDNS(fallback bool) { - settings := l.GetSettings() - - // Try with user provided plaintext ip address - targetIP := settings.PlaintextAddress - if targetIP != nil { - if fallback { - l.logger.Info("falling back on plaintext DNS at address " + targetIP.String()) - } else { - l.logger.Info("using plaintext DNS at address " + targetIP.String()) - } - nameserver.UseDNSInternally(targetIP) - err := nameserver.UseDNSSystemWide(l.resolvConf, targetIP, settings.KeepNameserver) - if err != nil { - l.logger.Error(err.Error()) - } - return - } - - provider := settings.Unbound.Providers[0] - targetIP = provider.DoT().IPv4[0] - if fallback { - l.logger.Info("falling back on plaintext DNS at address " + targetIP.String()) - } else { - l.logger.Info("using plaintext DNS at address " + targetIP.String()) - } - nameserver.UseDNSInternally(targetIP) - err := nameserver.UseDNSSystemWide(l.resolvConf, targetIP, settings.KeepNameserver) - if err != nil { - l.logger.Error(err.Error()) - } -} - -func (l *looper) RunRestartTicker(ctx context.Context, done chan<- struct{}) { - defer close(done) - // Timer that acts as a ticker - timer := time.NewTimer(time.Hour) - timer.Stop() - timerIsStopped := true - settings := l.GetSettings() - if settings.UpdatePeriod > 0 { - timer.Reset(settings.UpdatePeriod) - timerIsStopped = false - } - lastTick := time.Unix(0, 0) - for { - select { - case <-ctx.Done(): - if !timerIsStopped && !timer.Stop() { - <-timer.C - } - return - case <-timer.C: - lastTick = l.timeNow() - - status := l.GetStatus() - if status == constants.Running { - if err := l.updateFiles(ctx); err != nil { - l.state.SetStatus(constants.Crashed) - l.logger.Error(err.Error()) - l.logger.Warn("skipping Unbound restart due to failed files update") - continue - } - } - - _, _ = l.ApplyStatus(ctx, constants.Stopped) - _, _ = l.ApplyStatus(ctx, constants.Running) - - settings := l.GetSettings() - timer.Reset(settings.UpdatePeriod) - case <-l.updateTicker: - if !timer.Stop() { - <-timer.C - } - timerIsStopped = true - settings := l.GetSettings() - newUpdatePeriod := settings.UpdatePeriod - if newUpdatePeriod == 0 { - continue - } - var waited time.Duration - if lastTick.UnixNano() != 0 { - waited = l.timeSince(lastTick) - } - leftToWait := newUpdatePeriod - waited - timer.Reset(leftToWait) - timerIsStopped = false - } - } -} - -func (l *looper) updateFiles(ctx context.Context) (err error) { - l.logger.Info("downloading DNS over TLS cryptographic files") - if err := l.conf.SetupFiles(ctx); err != nil { - return err - } - settings := l.GetSettings() - - l.logger.Info("downloading hostnames and IP block lists") - blockedHostnames, blockedIPs, blockedIPPrefixes, errs := l.blockBuilder.All( - ctx, settings.BlacklistBuild) - for _, err := range errs { - l.logger.Warn(err.Error()) - } - - // TODO change to BlockHostnames() when migrating to qdm12/dns v2 - settings.Unbound.Blacklist.FqdnHostnames = blockedHostnames - settings.Unbound.Blacklist.IPs = blockedIPs - settings.Unbound.Blacklist.IPPrefixes = blockedIPPrefixes - - return l.conf.MakeUnboundConf(settings.Unbound) -} - -func (l *looper) GetStatus() (status models.LoopStatus) { return l.state.GetStatus() } -func (l *looper) ApplyStatus(ctx context.Context, status models.LoopStatus) ( - outcome string, err error) { - return l.state.ApplyStatus(ctx, status) -} -func (l *looper) GetSettings() (settings configuration.DNS) { return l.state.GetSettings() } -func (l *looper) SetSettings(ctx context.Context, settings configuration.DNS) ( - outcome string) { - return l.state.SetSettings(ctx, settings) -} diff --git a/internal/dns/plaintext.go b/internal/dns/plaintext.go new file mode 100644 index 00000000..aa59960e --- /dev/null +++ b/internal/dns/plaintext.go @@ -0,0 +1,36 @@ +package dns + +import "github.com/qdm12/dns/pkg/nameserver" + +func (l *looper) useUnencryptedDNS(fallback bool) { + settings := l.GetSettings() + + // Try with user provided plaintext ip address + targetIP := settings.PlaintextAddress + if targetIP != nil { + if fallback { + l.logger.Info("falling back on plaintext DNS at address " + targetIP.String()) + } else { + l.logger.Info("using plaintext DNS at address " + targetIP.String()) + } + nameserver.UseDNSInternally(targetIP) + err := nameserver.UseDNSSystemWide(l.resolvConf, targetIP, settings.KeepNameserver) + if err != nil { + l.logger.Error(err.Error()) + } + return + } + + provider := settings.Unbound.Providers[0] + targetIP = provider.DoT().IPv4[0] + if fallback { + l.logger.Info("falling back on plaintext DNS at address " + targetIP.String()) + } else { + l.logger.Info("using plaintext DNS at address " + targetIP.String()) + } + nameserver.UseDNSInternally(targetIP) + err := nameserver.UseDNSSystemWide(l.resolvConf, targetIP, settings.KeepNameserver) + if err != nil { + l.logger.Error(err.Error()) + } +} diff --git a/internal/dns/run.go b/internal/dns/run.go new file mode 100644 index 00000000..be08232c --- /dev/null +++ b/internal/dns/run.go @@ -0,0 +1,97 @@ +package dns + +import ( + "context" + "errors" + + "github.com/qdm12/gluetun/internal/constants" +) + +func (l *looper) Run(ctx context.Context, done chan<- struct{}) { + defer close(done) + + const fallback = false + l.useUnencryptedDNS(fallback) // TODO remove? Use default DNS by default for Docker resolution? + // TODO this one is kept if DNS_KEEP_NAMESERVER=on and should be replaced + + select { + case <-l.start: + case <-ctx.Done(): + return + } + + for ctx.Err() == nil { + // Upper scope variables for Unbound only + // Their values are to be used if DOT=off + waitError := make(chan error) + unboundCancel := func() { waitError <- nil } + closeStreams := func() {} + + for l.GetSettings().Enabled { + var err error + unboundCancel, waitError, closeStreams, err = l.setupUnbound(ctx) + if err == nil { + l.backoffTime = defaultBackoffTime + l.logger.Info("ready") + l.signalOrSetStatus(constants.Running) + break + } + + l.signalOrSetStatus(constants.Crashed) + + if ctx.Err() != nil { + return + } + + if !errors.Is(err, errUpdateFiles) { + const fallback = true + l.useUnencryptedDNS(fallback) + } + l.logAndWait(ctx, err) + } + + if !l.GetSettings().Enabled { + const fallback = false + l.useUnencryptedDNS(fallback) + } + + l.userTrigger = false + + stayHere := true + for stayHere { + select { + case <-ctx.Done(): + unboundCancel() + <-waitError + close(waitError) + closeStreams() + return + case <-l.stop: + l.userTrigger = true + l.logger.Info("stopping") + const fallback = false + l.useUnencryptedDNS(fallback) + unboundCancel() + <-waitError + // do not close waitError or the waitError + // select case will trigger + closeStreams() + l.stopped <- struct{}{} + case <-l.start: + l.userTrigger = true + l.logger.Info("starting") + stayHere = false + case err := <-waitError: // unexpected error + close(waitError) + closeStreams() + + unboundCancel() + l.state.SetStatus(constants.Crashed) + const fallback = true + l.useUnencryptedDNS(fallback) + l.logAndWait(ctx, err) + stayHere = false + } + } + } +} diff --git a/internal/dns/settings.go b/internal/dns/settings.go new file mode 100644 index 00000000..363d0d29 --- /dev/null +++ b/internal/dns/settings.go @@ -0,0 +1,19 @@ +package dns + +import ( + "context" + + "github.com/qdm12/gluetun/internal/configuration" +) + +func (l *looper) GetSettings() (settings configuration.DNS) { return l.state.GetSettings() } + +type SettingsSetter interface { + SetSettings(ctx context.Context, settings configuration.DNS) ( + outcome string) +} + +func (l *looper) SetSettings(ctx context.Context, settings configuration.DNS) ( + outcome string) { + return l.state.SetSettings(ctx, settings) +} diff --git a/internal/dns/setup.go b/internal/dns/setup.go new file mode 100644 index 00000000..fbeeaa6e --- /dev/null +++ b/internal/dns/setup.go @@ -0,0 +1,58 @@ +package dns + +import ( + "context" + "errors" + "net" + + "github.com/qdm12/dns/pkg/check" + "github.com/qdm12/dns/pkg/nameserver" +) + +var errUpdateFiles = errors.New("cannot update files") + +// Returning cancel == nil signals we want to re-run setupUnbound +// Returning err == errUpdateFiles signals we should not fall back +// on the plaintext DNS as DOT is still up and running. +func (l *looper) setupUnbound(ctx context.Context) ( + cancel context.CancelFunc, waitError chan error, closeStreams func(), err error) { + err = l.updateFiles(ctx) + if err != nil { + return nil, nil, nil, errUpdateFiles + } + + settings := l.GetSettings() + + unboundCtx, cancel := context.WithCancel(context.Background()) + stdoutLines, stderrLines, waitError, err := l.conf.Start(unboundCtx, settings.Unbound.VerbosityDetailsLevel) + if err != nil { + cancel() + return nil, nil, nil, err + } + + collectLinesDone := make(chan struct{}) + go l.collectLines(stdoutLines, stderrLines, collectLinesDone) + closeStreams = func() { + close(stdoutLines) + close(stderrLines) + <-collectLinesDone + } + + // use Unbound + nameserver.UseDNSInternally(net.IP{127, 0, 0, 1}) + err = nameserver.UseDNSSystemWide(l.resolvConf, net.IP{127, 0, 0, 1}, + settings.KeepNameserver) + if err != nil { + l.logger.Error(err.Error()) + } + + if err := check.WaitForDNS(ctx, net.DefaultResolver); err != nil { + cancel() + <-waitError + close(waitError) + closeStreams() + return nil, nil, nil, err + } + + return cancel, waitError, closeStreams, nil +} diff --git a/internal/dns/status.go b/internal/dns/status.go new file mode 100644 index 00000000..353e9316 --- /dev/null +++ b/internal/dns/status.go @@ -0,0 +1,19 @@ +package dns + +import ( + "context" + + "github.com/qdm12/gluetun/internal/models" +) + +func (l *looper) GetStatus() (status models.LoopStatus) { return l.state.GetStatus() } + +type StatusApplier interface { + ApplyStatus(ctx context.Context, status models.LoopStatus) ( + outcome string, err error) +} + +func (l *looper) ApplyStatus(ctx context.Context, status models.LoopStatus) ( + outcome string, err error) { + return l.state.ApplyStatus(ctx, status) +} diff --git a/internal/dns/ticker.go b/internal/dns/ticker.go new file mode 100644 index 00000000..9c5a1352 --- /dev/null +++ b/internal/dns/ticker.go @@ -0,0 +1,66 @@ +package dns + +import ( + "context" + "time" + + "github.com/qdm12/gluetun/internal/constants" +) + +func (l *looper) RunRestartTicker(ctx context.Context, done chan<- struct{}) { + defer close(done) + // Timer that acts as a ticker + timer := time.NewTimer(time.Hour) + timer.Stop() + timerIsStopped := true + settings := l.GetSettings() + if settings.UpdatePeriod > 0 { + timer.Reset(settings.UpdatePeriod) + timerIsStopped = false + } + lastTick := time.Unix(0, 0) + for { + select { + case <-ctx.Done(): + if !timerIsStopped && !timer.Stop() { + <-timer.C + } + return + case <-timer.C: + lastTick = l.timeNow() + + status := l.GetStatus() + if status == constants.Running { + if err := l.updateFiles(ctx); err != nil { + l.state.SetStatus(constants.Crashed) + l.logger.Error(err.Error()) + l.logger.Warn("skipping Unbound restart due to failed files update") + continue + } + } + + _, _ = l.ApplyStatus(ctx, constants.Stopped) + _, _ = l.ApplyStatus(ctx, constants.Running) + + settings := l.GetSettings() + timer.Reset(settings.UpdatePeriod) + case <-l.updateTicker: + if !timer.Stop() { + <-timer.C + } + timerIsStopped = true + settings := l.GetSettings() + newUpdatePeriod := settings.UpdatePeriod + if newUpdatePeriod == 0 { + continue + } + var waited time.Duration + if lastTick.UnixNano() != 0 { + waited = l.timeSince(lastTick) + } + leftToWait := newUpdatePeriod - waited + timer.Reset(leftToWait) + timerIsStopped = false + } + } +} diff --git a/internal/dns/update.go b/internal/dns/update.go new file mode 100644 index 00000000..3f10acbf --- /dev/null +++ b/internal/dns/update.go @@ -0,0 +1,25 @@ +package dns + +import "context" + +func (l *looper) updateFiles(ctx context.Context) (err error) { + l.logger.Info("downloading DNS over TLS cryptographic files") + if err := l.conf.SetupFiles(ctx); err != nil { + return err + } + settings := l.GetSettings() + + l.logger.Info("downloading hostnames and IP block lists") + blockedHostnames, blockedIPs, blockedIPPrefixes, errs := l.blockBuilder.All( + ctx, settings.BlacklistBuild) + for _, err := range errs { + l.logger.Warn(err.Error()) + } + + // TODO change to BlockHostnames() when migrating to qdm12/dns v2 + settings.Unbound.Blacklist.FqdnHostnames = blockedHostnames + settings.Unbound.Blacklist.IPs = blockedIPs + settings.Unbound.Blacklist.IPPrefixes = blockedIPPrefixes + + return l.conf.MakeUnboundConf(settings.Unbound) +}