osbase/net/dns/forward: add DNS forward handler

This adds a DNS server handler for forwarding queries to upstream DNS
resolvers, with a built-in cache. The implementation is partially based
on CoreDNS. The proxy, cache and up packages are only lightly modified.
The forward package itself however is mostly new code. Unlike CoreDNS,
it supports changing upstreams at runtime, and has integrated caching
and answer order randomization.

Some improvements over CoreDNS:
- Concurrent identical queries only result in one upstream query.
- In case of errors, Extended DNS Errors are added to replies.
- Very large replies are not stored in the cache to avoid using too much
memory.

Change-Id: I42294ae4997d621a6e55c98e46a04874eab75c99
Reviewed-on: https://review.monogon.dev/c/monogon/+/3258
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
Tested-by: Jenkins CI
diff --git a/osbase/net/dns/forward/up/BUILD.bazel b/osbase/net/dns/forward/up/BUILD.bazel
new file mode 100644
index 0000000..b9f2683
--- /dev/null
+++ b/osbase/net/dns/forward/up/BUILD.bazel
@@ -0,0 +1,14 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "up",
+    srcs = ["up.go"],
+    importpath = "source.monogon.dev/osbase/net/dns/forward/up",
+    visibility = ["//osbase/net/dns/forward:__subpackages__"],
+)
+
+go_test(
+    name = "up_test",
+    srcs = ["up_test.go"],
+    embed = [":up"],
+)
diff --git a/osbase/net/dns/forward/up/up.go b/osbase/net/dns/forward/up/up.go
new file mode 100644
index 0000000..9806319
--- /dev/null
+++ b/osbase/net/dns/forward/up/up.go
@@ -0,0 +1,93 @@
+// Package up is used to run a function for some duration. If a new function is
+// added while a previous run is still ongoing, nothing new will be executed.
+package up
+
+// Taken and modified from CoreDNS, under Apache 2.0.
+
+import (
+	"sync"
+	"time"
+)
+
+// Probe is used to run a single Func until it returns true
+// (indicating a target is healthy).
+// If an Func is already in progress no new one will be added,
+// i.e. there is always a maximum of 1 checks in flight.
+//
+// There is a tradeoff to be made in figuring out quickly that an upstream is
+// healthy and not doing much work (sending queries) to find that out.
+// Having some kind of exp. backoff here won't help much, because you don't
+// want to backoff too much. You then also need random queries to be performed
+// every so often to quickly detect a working upstream. In the end we just send
+// a query every 0.5 second to check the upstream. This hopefully strikes a
+// balance between getting information about the upstream state quickly and not
+// doing too much work. Note that 0.5s is still an eternity in DNS, so we may
+// actually want to shorten it.
+type Probe struct {
+	sync.Mutex
+	inprogress int
+	interval   time.Duration
+}
+
+// Func is used to determine if a target is alive.
+// If so this function must return nil.
+type Func func() error
+
+// New returns a pointer to an initialized Probe.
+func New() *Probe { return &Probe{} }
+
+// Do will probe target, if a probe is already in progress this is a noop.
+func (p *Probe) Do(f Func) {
+	p.Lock()
+	if p.inprogress != idle {
+		p.Unlock()
+		return
+	}
+	p.inprogress = active
+	interval := p.interval
+	p.Unlock()
+	// Passed the lock. Now run f for as long it returns false.
+	// If a true is returned we return from the goroutine
+	// and we can accept another Func to run.
+	go func() {
+		i := 1
+		for {
+			if err := f(); err == nil {
+				break
+			}
+			time.Sleep(interval)
+			p.Lock()
+			if p.inprogress == stop {
+				p.Unlock()
+				return
+			}
+			p.Unlock()
+			i++
+		}
+
+		p.Lock()
+		p.inprogress = idle
+		p.Unlock()
+	}()
+}
+
+// Stop stops the probing.
+func (p *Probe) Stop() {
+	p.Lock()
+	p.inprogress = stop
+	p.Unlock()
+}
+
+// Start will initialize the probe manager,
+// after which probes can be initiated with Do.
+func (p *Probe) Start(interval time.Duration) {
+	p.Lock()
+	p.interval = interval
+	p.Unlock()
+}
+
+const (
+	idle = iota
+	active
+	stop
+)
diff --git a/osbase/net/dns/forward/up/up_test.go b/osbase/net/dns/forward/up/up_test.go
new file mode 100644
index 0000000..0d0f928
--- /dev/null
+++ b/osbase/net/dns/forward/up/up_test.go
@@ -0,0 +1,42 @@
+package up
+
+// Taken and modified from CoreDNS, under Apache 2.0.
+
+import (
+	"sync"
+	"sync/atomic"
+	"testing"
+	"time"
+)
+
+func TestUp(t *testing.T) {
+	pr := New()
+	wg := sync.WaitGroup{}
+	hits := int32(0)
+
+	upfunc := func() error {
+		atomic.AddInt32(&hits, 1)
+		// Sleep tiny amount so that our other pr.Do() calls hit the lock.
+		time.Sleep(3 * time.Millisecond)
+		wg.Done()
+		return nil
+	}
+
+	pr.Start(5 * time.Millisecond)
+	defer pr.Stop()
+
+	// These functions AddInt32 to the same hits variable, but we only want to
+	// wait when upfunc finishes, as that only calls Done() on the waitgroup.
+	upfuncNoWg := func() error { atomic.AddInt32(&hits, 1); return nil }
+	wg.Add(1)
+	pr.Do(upfunc)
+	pr.Do(upfuncNoWg)
+	pr.Do(upfuncNoWg)
+
+	wg.Wait()
+
+	h := atomic.LoadInt32(&hits)
+	if h != 1 {
+		t.Errorf("Expected hits to be %d, got %d", 1, h)
+	}
+}