prometheus-dnssec-exporter/main.go
2022-04-08 21:28:31 +02:00

318 lines
8.4 KiB
Go

package main
import (
"fmt"
"io/ioutil"
"net/http"
"os"
"sort"
"strings"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/miekg/dns"
prom "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/prometheus/common/promlog"
"github.com/prometheus/common/promlog/flag"
"github.com/prometheus/common/version"
"github.com/prometheus/exporter-toolkit/web"
kingpin "gopkg.in/alecthomas/kingpin.v2"
"gopkg.in/yaml.v2"
)
const namespace = "dnssec"
type MetricsCollector struct {
Log log.Logger
signatureOkMetric *prom.GaugeVec
sokRcodeMetric *prom.GaugeVec
dsCountMetric *prom.GaugeVec
dsRcodeMetric *prom.GaugeVec
cdsCountMetric *prom.GaugeVec
cdsRcodeMetric *prom.GaugeVec
cdsDsMatchMetric *prom.GaugeVec
zones []string
DNS dns.Client
Resolver string
}
type DnsConfig struct {
Resolver string `yaml:"resolver"`
Zones []string `yaml:"zones"`
RRTypes []string `yaml:"rrtypes"`
}
type DnssecConfig struct {
DnsConfig DnsConfig `yaml:"dns"`
}
func b2f64(b bool) float64 {
if b {
return 1
}
return 0
}
func NewMetricsCollector(zonelist []string, resolver string, logger log.Logger) *MetricsCollector {
signatureOkMetric := prom.NewGaugeVec(
prom.GaugeOpts{
Namespace: namespace,
Name: "signature_ok",
Help: "1 if the DNSSEC signature is present and valid, 0 otherwise",
},
[]string{"zone", "tld", "parent"},
)
sokRcodeMetric := prom.NewGaugeVec(
prom.GaugeOpts{
Namespace: namespace,
Name: "signature_rcode",
Help: "RCode of the DNS query",
},
[]string{"zone", "tld", "parent"},
)
dsCountMetric := prom.NewGaugeVec(
prom.GaugeOpts{
Namespace: namespace,
Name: "ds_count",
Help: "Number of DS record is present in the parent zone",
},
[]string{"zone", "tld", "parent"},
)
dsRcodeMetric := prom.NewGaugeVec(
prom.GaugeOpts{
Namespace: namespace,
Name: "ds_rcode",
Help: "RCode of the DS record answer",
},
[]string{"zone", "tld", "parent"},
)
cdsCountMetric := prom.NewGaugeVec(
prom.GaugeOpts{
Namespace: namespace,
Name: "cds_count",
Help: "Number of CDS records present in the zone",
},
[]string{"zone", "tld", "parent"},
)
cdsRcodeMetric := prom.NewGaugeVec(
prom.GaugeOpts{
Namespace: namespace,
Name: "cds_rcode",
Help: "RCode of the CDS record answer",
},
[]string{"zone", "tld", "parent"},
)
cdsDsMatchMetric := prom.NewGaugeVec(
prom.GaugeOpts{
Namespace: namespace,
Name: "cds_ds_match",
Help: "1 if the CDS and DS records match, 0 otherwise",
},
[]string{"zone", "tld", "parent"},
)
collector := MetricsCollector{
signatureOkMetric: signatureOkMetric,
sokRcodeMetric: sokRcodeMetric,
dsCountMetric: dsCountMetric,
dsRcodeMetric: dsRcodeMetric,
cdsCountMetric: cdsCountMetric,
cdsRcodeMetric: cdsRcodeMetric,
cdsDsMatchMetric: cdsDsMatchMetric,
zones: zonelist,
Resolver: resolver,
Log: logger,
}
return &collector
}
func (c *MetricsCollector) Describe(ch chan<- *prom.Desc) {
c.signatureOkMetric.Describe(ch)
c.sokRcodeMetric.Describe(ch)
c.dsCountMetric.Describe(ch)
c.dsRcodeMetric.Describe(ch)
c.cdsCountMetric.Describe(ch)
c.cdsRcodeMetric.Describe(ch)
c.cdsDsMatchMetric.Describe(ch)
}
func (c *MetricsCollector) Collect(ch chan<- prom.Metric) {
for _, zone := range c.zones {
labels := strings.Split(zone, ".")
tld := strings.Join(labels[len(labels)-2:], ".")
parent := strings.Join(labels[1:], ".")
promlabels := prom.Labels{
"zone": zone,
"tld": tld,
"parent": parent,
}
sok, sokrcode, _ := c.signatureOk(zone)
c.sokRcodeMetric.With(promlabels).Set(float64(sokrcode))
c.signatureOkMetric.With(promlabels).Set(b2f64(sok))
dscount, dsrcode, cdscount, cdsrcode, cdm, _ := c.cdsDsMatches(zone)
c.dsCountMetric.With(promlabels).Set(float64(dscount))
c.dsRcodeMetric.With(promlabels).Set(float64(dsrcode))
c.cdsCountMetric.With(promlabels).Set(float64(cdscount))
c.cdsRcodeMetric.With(promlabels).Set(float64(cdsrcode))
c.cdsDsMatchMetric.With(promlabels).Set(b2f64(cdm))
}
c.signatureOkMetric.Collect(ch)
c.sokRcodeMetric.Collect(ch)
c.dsCountMetric.Collect(ch)
c.dsRcodeMetric.Collect(ch)
c.cdsCountMetric.Collect(ch)
c.cdsRcodeMetric.Collect(ch)
c.cdsDsMatchMetric.Collect(ch)
}
type DsData struct {
KeyTag uint16
Algorithm uint8
DigestType uint8
Digest string
}
func Equal(a, b []DsData) bool {
if len(a) != len(b) {
return false
}
for i, v := range a {
if v != b[i] {
return false
}
}
return true
}
func (c *MetricsCollector) signatureOk(zone string) (bool, int, error) {
qsoa := new(dns.Msg)
// Request DNSSEC information
qsoa.SetEdns0(4096, true)
qsoa.SetQuestion(dns.Fqdn(zone), dns.TypeSOA)
rsoa, _, err := c.DNS.Exchange(qsoa, c.Resolver)
if err != nil {
return false, rsoa.MsgHdr.Rcode, err
}
_ = level.Debug(c.Log).Log(fmt.Sprintf("SOA header: %+v\n", rsoa.MsgHdr))
return rsoa.MsgHdr.AuthenticatedData && rsoa.MsgHdr.Rcode == dns.RcodeSuccess, rsoa.MsgHdr.Rcode, nil
}
func (c *MetricsCollector) cdsDsMatches(zone string) (int, int, int, int, bool, error) {
dsCount := 0
cdsCount := 0
match := false
var dsdata []DsData
var cdsdata []DsData
qcds := new(dns.Msg)
qcds.SetQuestion(dns.Fqdn(zone), dns.TypeCDS)
rcds, _, err1 := c.DNS.Exchange(qcds, c.Resolver)
cdsRcode := rcds.MsgHdr.Rcode
if err1 == nil {
cdsCount = len(rcds.Answer)
cdsdata = make([]DsData, len(rcds.Answer))
_ = level.Debug(c.Log).Log(fmt.Sprintf("CDS data: %+v\n", rcds.Answer))
for i, a := range rcds.Answer {
ds := a.(*dns.CDS).DS
cdsdata[i] = DsData{
KeyTag: ds.KeyTag,
Algorithm: ds.Algorithm,
DigestType: ds.DigestType,
Digest: ds.Digest,
}
}
sort.SliceStable(cdsdata, func(a, b int) bool { return cdsdata[a].KeyTag < cdsdata[b].KeyTag })
}
qds := new(dns.Msg)
qds.SetQuestion(dns.Fqdn(zone), dns.TypeDS)
rds, _, err2 := c.DNS.Exchange(qds, c.Resolver)
dsRcode := rds.MsgHdr.Rcode
if err2 == nil {
dsCount = len(rds.Answer)
dsdata = make([]DsData, len(rds.Answer))
_ = level.Debug(c.Log).Log(fmt.Sprintf("DS data: %+v\n", rds.Answer))
for i, a := range rds.Answer {
ds := a.(*dns.DS)
dsdata[i] = DsData{
KeyTag: ds.KeyTag,
Algorithm: ds.Algorithm,
DigestType: ds.DigestType,
Digest: ds.Digest,
}
}
sort.SliceStable(dsdata, func(a, b int) bool { return dsdata[a].KeyTag < dsdata[b].KeyTag })
}
if dsdata != nil && cdsdata != nil {
match = cdsCount > 0 && Equal(dsdata, cdsdata)
// special case: removal requested
if len(cdsdata) == 1 && len(dsdata) == 0 && cdsdata[0].Algorithm == 0 {
match = true
}
}
return dsCount, dsRcode, cdsCount, cdsRcode, match, nil
}
func main() {
var (
listenAddress = kingpin.Flag(
"web.listen-address",
"Address on which to expose metrics.",
).Default(":9142").String()
metricsPath = kingpin.Flag(
"web.metrics-path",
"Path under which to expose metrics.",
).Default("/metrics").String()
webConfigFile = kingpin.Flag(
"web.config",
"Path to web config yaml file.",
).Default("").String()
configFile = kingpin.Flag(
"config",
"Path to config yaml file.",
).Default("").String()
)
promlogConfig := &promlog.Config{}
flag.AddFlags(kingpin.CommandLine, promlogConfig)
kingpin.Version(version.Print("dnssec_exporter"))
kingpin.CommandLine.UsageWriter(os.Stdout)
kingpin.HelpFlag.Short('h')
kingpin.Parse()
logger := promlog.New(promlogConfig)
logger = level.NewFilter(logger, level.AllowDebug())
config := DnssecConfig{
DnsConfig: DnsConfig{
Zones: []string{"example.org"},
Resolver: "1.1.1.1:53",
},
}
if configFile != nil {
content, err := ioutil.ReadFile(*configFile)
if err != nil {
_ = level.Error(logger).Log(err.Error())
os.Exit(1)
}
err = yaml.Unmarshal(content, &config)
if err != nil {
_ = level.Error(logger).Log(err.Error())
os.Exit(1)
}
}
// Always add trailing dot
for i, z := range config.DnsConfig.Zones {
if z[len(z)-1] != byte('.') {
config.DnsConfig.Zones[i] = z + "."
}
}
metricsCollector := NewMetricsCollector(config.DnsConfig.Zones, config.DnsConfig.Resolver, logger)
prom.MustRegister(metricsCollector)
http.Handle(*metricsPath, promhttp.Handler())
server := &http.Server{Addr: *listenAddress}
_ = logger.Log(web.ListenAndServe(server, *webConfigFile, logger))
}