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/metrics.go b/osbase/net/dns/forward/metrics.go
new file mode 100644
index 0000000..4977b02
--- /dev/null
+++ b/osbase/net/dns/forward/metrics.go
@@ -0,0 +1,59 @@
+package forward
+
+// Taken and modified from CoreDNS, under Apache 2.0.
+
+import (
+	"github.com/prometheus/client_golang/prometheus"
+
+	"source.monogon.dev/osbase/net/dns"
+)
+
+// Variables declared for monitoring.
+var (
+	// Possible results:
+	//   * hit: Item found and returned from cache.
+	//   * miss: Item not found in cache.
+	//   * refresh: Item found in cache, but is either expired, or
+	//     truncated while the client used TCP.
+	cacheLookupsCount = dns.MetricsFactory.NewCounterVec(prometheus.CounterOpts{
+		Namespace: "dnsserver",
+		Subsystem: "forward",
+		Name:      "cache_lookups_total",
+		Help:      "Counter of the number of cache lookups.",
+	}, []string{"result"})
+
+	// protocol is one of:
+	//   * udp
+	//   * udp_truncated
+	//   * tcp
+	// rcode can be an uppercase rcode name, a numeric rcode if the rcode is not
+	// known, or one of:
+	//   * timeout
+	//   * network_error
+	//   * protocol_error
+	upstreamDuration = dns.MetricsFactory.NewHistogramVec(prometheus.HistogramOpts{
+		Namespace: "dnsserver",
+		Subsystem: "forward",
+		Name:      "upstream_duration_seconds",
+		Buckets:   prometheus.ExponentialBuckets(0.00025, 2, 16), // from 0.25ms to 8 seconds
+		Help:      "Histogram of the time each upstream request took.",
+	}, []string{"to", "protocol", "rcode"})
+
+	// Possible reasons:
+	//   * concurrency_limit: Too many concurrent upstream queries.
+	//   * no_upstreams: There are no upstreams configured.
+	//   * no_recursion_desired: Client did not set Recursion Desired flag.
+	rejectsCount = dns.MetricsFactory.NewCounterVec(prometheus.CounterOpts{
+		Namespace: "dnsserver",
+		Subsystem: "forward",
+		Name:      "rejects_total",
+		Help:      "Counter of the number of queries rejected and not forwarded to an upstream.",
+	}, []string{"reason"})
+
+	healthcheckBrokenCount = dns.MetricsFactory.NewCounter(prometheus.CounterOpts{
+		Namespace: "dnsserver",
+		Subsystem: "forward",
+		Name:      "healthcheck_broken_total",
+		Help:      "Counter of the number of complete failures of the healthchecks.",
+	})
+)