Config file support, more metrics

This commit is contained in:
s3lph 2022-04-08 21:21:58 +02:00
parent b175d73156
commit eddafb699b
3 changed files with 165 additions and 127 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
prometheus-dnssec-exporter prometheus-dnssec-exporter
config.yaml

2
go.mod
View file

@ -9,6 +9,7 @@ require (
github.com/prometheus/common v0.29.0 github.com/prometheus/common v0.29.0
github.com/prometheus/exporter-toolkit v0.7.1 github.com/prometheus/exporter-toolkit v0.7.1
gopkg.in/alecthomas/kingpin.v2 v2.2.6 gopkg.in/alecthomas/kingpin.v2 v2.2.6
gopkg.in/yaml.v2 v2.4.0
) )
require ( require (
@ -35,5 +36,4 @@ require (
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/appengine v1.6.6 // indirect google.golang.org/appengine v1.6.6 // indirect
google.golang.org/protobuf v1.26.0-rc.1 // indirect google.golang.org/protobuf v1.26.0-rc.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
) )

289
main.go
View file

@ -2,6 +2,7 @@ package main
import ( import (
"fmt" "fmt"
"io/ioutil"
"net/http" "net/http"
"os" "os"
"sort" "sort"
@ -17,28 +18,42 @@ import (
"github.com/prometheus/common/version" "github.com/prometheus/common/version"
"github.com/prometheus/exporter-toolkit/web" "github.com/prometheus/exporter-toolkit/web"
kingpin "gopkg.in/alecthomas/kingpin.v2" kingpin "gopkg.in/alecthomas/kingpin.v2"
"gopkg.in/yaml.v2"
) )
const namespace = "dnssec" const namespace = "dnssec"
var removalCds = DsData{
KeyTag: 0,
Algorithm: 0,
DigestType: 0,
Digest: "00",
}
type MetricsCollector struct { type MetricsCollector struct {
Log log.Logger Log log.Logger
signatureOkMetric *prom.GaugeVec signatureOkMetric *prom.GaugeVec
dsPresentMetric *prom.GaugeVec sokRcodeMetric *prom.GaugeVec
cdsPresentMetric *prom.GaugeVec dsCountMetric *prom.GaugeVec
dsRcodeMetric *prom.GaugeVec
cdsCountMetric *prom.GaugeVec
cdsRcodeMetric *prom.GaugeVec
cdsDsMatchMetric *prom.GaugeVec cdsDsMatchMetric *prom.GaugeVec
zones []string zones []string
DNS dns.Client DNS dns.Client
Resolver string 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 { func NewMetricsCollector(zonelist []string, resolver string, logger log.Logger) *MetricsCollector {
signatureOkMetric := prom.NewGaugeVec( signatureOkMetric := prom.NewGaugeVec(
prom.GaugeOpts{ prom.GaugeOpts{
@ -48,19 +63,43 @@ func NewMetricsCollector(zonelist []string, resolver string, logger log.Logger)
}, },
[]string{"zone", "tld", "parent"}, []string{"zone", "tld", "parent"},
) )
dsPresentMetric := prom.NewGaugeVec( sokRcodeMetric := prom.NewGaugeVec(
prom.GaugeOpts{ prom.GaugeOpts{
Namespace: namespace, Namespace: namespace,
Name: "ds_present", Name: "signature_rcode",
Help: "1 if CDS record is present in the parent zone, 0 otherwise", Help: "RCode of the DNS query",
}, },
[]string{"zone", "tld", "parent"}, []string{"zone", "tld", "parent"},
) )
cdsPresentMetric := prom.NewGaugeVec( dsCountMetric := prom.NewGaugeVec(
prom.GaugeOpts{ prom.GaugeOpts{
Namespace: namespace, Namespace: namespace,
Name: "cds_present", Name: "ds_count",
Help: "1 if a CDS record is present in the zone, 0 otherwise", 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"}, []string{"zone", "tld", "parent"},
) )
@ -74,8 +113,11 @@ func NewMetricsCollector(zonelist []string, resolver string, logger log.Logger)
) )
collector := MetricsCollector{ collector := MetricsCollector{
signatureOkMetric: signatureOkMetric, signatureOkMetric: signatureOkMetric,
dsPresentMetric: dsPresentMetric, sokRcodeMetric: sokRcodeMetric,
cdsPresentMetric: cdsPresentMetric, dsCountMetric: dsCountMetric,
dsRcodeMetric: dsRcodeMetric,
cdsCountMetric: cdsCountMetric,
cdsRcodeMetric: cdsRcodeMetric,
cdsDsMatchMetric: cdsDsMatchMetric, cdsDsMatchMetric: cdsDsMatchMetric,
zones: zonelist, zones: zonelist,
Resolver: resolver, Resolver: resolver,
@ -86,29 +128,16 @@ func NewMetricsCollector(zonelist []string, resolver string, logger log.Logger)
func (c *MetricsCollector) Describe(ch chan<- *prom.Desc) { func (c *MetricsCollector) Describe(ch chan<- *prom.Desc) {
c.signatureOkMetric.Describe(ch) c.signatureOkMetric.Describe(ch)
c.dsPresentMetric.Describe(ch) c.sokRcodeMetric.Describe(ch)
c.cdsPresentMetric.Describe(ch) c.dsCountMetric.Describe(ch)
c.dsRcodeMetric.Describe(ch)
c.cdsCountMetric.Describe(ch)
c.cdsRcodeMetric.Describe(ch)
c.cdsDsMatchMetric.Describe(ch) c.cdsDsMatchMetric.Describe(ch)
} }
func (c *MetricsCollector) Collect(ch chan<- prom.Metric) { func (c *MetricsCollector) Collect(ch chan<- prom.Metric) {
err := c.reportMetrics(ch)
if err != nil {
// TODO
c.Log.Log(err)
}
}
func (c *MetricsCollector) reportMetrics(ch chan<- prom.Metric) error {
for _, zone := range c.zones { for _, zone := range c.zones {
sok, err1 := c.signatureOk(zone)
if err1 != nil {
continue
}
dsp, cdp, cdm, err2 := c.cdsDsMatches(zone)
if err2 != nil {
continue
}
labels := strings.Split(zone, ".") labels := strings.Split(zone, ".")
tld := strings.Join(labels[len(labels)-2:], ".") tld := strings.Join(labels[len(labels)-2:], ".")
parent := strings.Join(labels[1:], ".") parent := strings.Join(labels[1:], ".")
@ -117,37 +146,23 @@ func (c *MetricsCollector) reportMetrics(ch chan<- prom.Metric) error {
"tld": tld, "tld": tld,
"parent": parent, "parent": parent,
} }
signatureOkMetric := c.signatureOkMetric.With(promlabels) sok, sokrcode, _ := c.signatureOk(zone)
dsPresentMetric := c.dsPresentMetric.With(promlabels) c.sokRcodeMetric.With(promlabels).Set(float64(sokrcode))
cdsPresentMetric := c.cdsPresentMetric.With(promlabels) c.signatureOkMetric.With(promlabels).Set(b2f64(sok))
cdsDsMatchMetric := c.cdsDsMatchMetric.With(promlabels) dscount, dsrcode, cdscount, cdsrcode, cdm, _ := c.cdsDsMatches(zone)
if sok { c.dsCountMetric.With(promlabels).Set(float64(dscount))
signatureOkMetric.Set(1) c.dsRcodeMetric.With(promlabels).Set(float64(dsrcode))
} else { c.cdsCountMetric.With(promlabels).Set(float64(cdscount))
signatureOkMetric.Set(0) c.cdsRcodeMetric.With(promlabels).Set(float64(cdsrcode))
} c.cdsDsMatchMetric.With(promlabels).Set(b2f64(cdm))
if dsp {
dsPresentMetric.Set(1)
} else {
dsPresentMetric.Set(0)
}
if cdp {
cdsPresentMetric.Set(1)
} else {
cdsPresentMetric.Set(0)
}
if cdm {
cdsDsMatchMetric.Set(1)
} else {
cdsDsMatchMetric.Set(0)
}
} }
c.signatureOkMetric.Collect(ch) c.signatureOkMetric.Collect(ch)
c.dsPresentMetric.Collect(ch) c.sokRcodeMetric.Collect(ch)
c.cdsPresentMetric.Collect(ch) c.dsCountMetric.Collect(ch)
c.dsRcodeMetric.Collect(ch)
c.cdsCountMetric.Collect(ch)
c.cdsRcodeMetric.Collect(ch)
c.cdsDsMatchMetric.Collect(ch) c.cdsDsMatchMetric.Collect(ch)
return nil
} }
type DsData struct { type DsData struct {
@ -169,69 +184,76 @@ func Equal(a, b []DsData) bool {
return true return true
} }
func (c *MetricsCollector) signatureOk(zone string) (bool, error) { func (c *MetricsCollector) signatureOk(zone string) (bool, int, error) {
qsoa := new(dns.Msg) qsoa := new(dns.Msg)
// Request DNSSEC information // Request DNSSEC information
qsoa.SetEdns0(4096, true) qsoa.SetEdns0(4096, true)
qsoa.SetQuestion(dns.Fqdn(zone), dns.TypeSOA) qsoa.SetQuestion(dns.Fqdn(zone), dns.TypeSOA)
rsoa, _, err := c.DNS.Exchange(qsoa, c.Resolver) rsoa, _, err := c.DNS.Exchange(qsoa, c.Resolver)
if err != nil { if err != nil {
return false, err return false, rsoa.MsgHdr.Rcode, err
} }
level.Debug(c.Log).Log(fmt.Sprintf("SOA header: %+v\n", rsoa.MsgHdr)) level.Debug(c.Log).Log(fmt.Sprintf("SOA header: %+v\n", rsoa.MsgHdr))
return rsoa.MsgHdr.AuthenticatedData && rsoa.MsgHdr.Rcode == dns.RcodeSuccess, nil return rsoa.MsgHdr.AuthenticatedData && rsoa.MsgHdr.Rcode == dns.RcodeSuccess, rsoa.MsgHdr.Rcode, nil
} }
func (c *MetricsCollector) cdsDsMatches(zone string) (bool, bool, bool, error) { func (c *MetricsCollector) cdsDsMatches(zone string) (int, int, int, int, bool, error) {
dsPresent := false dsCount := 0
cdsPresent := false dsRcode := -1
cdsCount := 0
cdsRcode := -1
match := false
var dsdata []DsData
var cdsdata []DsData
qcds := new(dns.Msg) qcds := new(dns.Msg)
qcds.SetQuestion(dns.Fqdn(zone), dns.TypeCDS) qcds.SetQuestion(dns.Fqdn(zone), dns.TypeCDS)
rcds, _, err := c.DNS.Exchange(qcds, c.Resolver) rcds, _, err1 := c.DNS.Exchange(qcds, c.Resolver)
if err != nil { cdsRcode = rcds.MsgHdr.Rcode
return dsPresent, cdsPresent, false, err if err1 == nil {
} cdsCount = len(rcds.Answer)
cdsPresent = len(rcds.Answer) > 0 cdsdata = make([]DsData, len(rcds.Answer))
cdsdata := make([]DsData, len(rcds.Answer)) level.Debug(c.Log).Log(fmt.Sprintf("CDS data: %+v\n", rcds.Answer))
level.Debug(c.Log).Log(fmt.Sprintf("CDS data: %+v\n", rcds.Answer)) for i, a := range rcds.Answer {
for i, a := range rcds.Answer { ds := a.(*dns.CDS).DS
ds := a.(*dns.CDS).DS cdsdata[i] = DsData{
cdsdata[i] = DsData{ KeyTag: ds.KeyTag,
KeyTag: ds.KeyTag, Algorithm: ds.Algorithm,
Algorithm: ds.Algorithm, DigestType: ds.DigestType,
DigestType: ds.DigestType, Digest: ds.Digest,
Digest: ds.Digest, }
} }
sort.SliceStable(cdsdata, func(a, b int) bool { return cdsdata[a].KeyTag < cdsdata[b].KeyTag })
} }
sort.SliceStable(cdsdata, func(a, b int) bool { return cdsdata[a].KeyTag < cdsdata[b].KeyTag })
qds := new(dns.Msg) qds := new(dns.Msg)
qds.SetQuestion(dns.Fqdn(zone), dns.TypeDS) qds.SetQuestion(dns.Fqdn(zone), dns.TypeDS)
rds, _, err := c.DNS.Exchange(qds, c.Resolver) rds, _, err2 := c.DNS.Exchange(qds, c.Resolver)
if err != nil { dsRcode = rds.MsgHdr.Rcode
return dsPresent, cdsPresent, false, err 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 })
} }
dsPresent = len(rds.Answer) > 0
dsdata := make([]DsData, len(rds.Answer)) if dsdata != nil && cdsdata != nil {
level.Debug(c.Log).Log(fmt.Sprintf("DS data: %+v\n", rds.Answer)) match = cdsCount > 0 && Equal(dsdata, cdsdata)
for i, a := range rds.Answer { // special case: removal requested
ds := a.(*dns.DS) if len(cdsdata) == 1 && len(dsdata) == 0 && cdsdata[0].Algorithm == 0 {
dsdata[i] = DsData{ match = true
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 }) return dsCount, dsRcode, cdsCount, cdsRcode, match, nil
match := cdsPresent && Equal(dsdata, cdsdata)
// special case: removal requested
if len(cdsdata) == 1 && len(dsdata) == 0 && cdsdata[0] == removalCds {
match = true
}
return dsPresent, cdsPresent, match, nil
} }
func main() { func main() {
@ -244,17 +266,13 @@ func main() {
"web.metrics-path", "web.metrics-path",
"Path under which to expose metrics.", "Path under which to expose metrics.",
).Default("/metrics").String() ).Default("/metrics").String()
resolver = kingpin.Flag( webConfigFile = kingpin.Flag(
"dns.resolver",
"DNS Resolver to use.",
).Default("127.0.0.1:53").String()
zones = kingpin.Flag(
"dns.zones",
"DNS zones to check, comma separated.",
).Default("example.org").String()
configFile = kingpin.Flag(
"web.config", "web.config",
"[EXPERIMENTAL] Path to config yaml file that can enable TLS or authentication.", "Path to web config yaml file.",
).Default("").String()
configFile = kingpin.Flag(
"config",
"Path to config yaml file.",
).Default("").String() ).Default("").String()
) )
@ -264,19 +282,38 @@ func main() {
kingpin.CommandLine.UsageWriter(os.Stdout) kingpin.CommandLine.UsageWriter(os.Stdout)
kingpin.HelpFlag.Short('h') kingpin.HelpFlag.Short('h')
kingpin.Parse() kingpin.Parse()
zonelist := strings.Split(*zones, ",")
// Always add trailing dot
for i, z := range zonelist {
if z[len(z)-1] != byte('.') {
zonelist[i] = z + "."
}
}
logger := promlog.New(promlogConfig) logger := promlog.New(promlogConfig)
logger = level.NewFilter(logger, level.AllowDebug()) logger = level.NewFilter(logger, level.AllowDebug())
logger = log.With(logger, "ts", log.DefaultTimestampUTC)
metricsCollector := NewMetricsCollector(zonelist, *resolver, logger) 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) prom.MustRegister(metricsCollector)
http.Handle(*metricsPath, promhttp.Handler()) http.Handle(*metricsPath, promhttp.Handler())
server := &http.Server{Addr: *listenAddress} server := &http.Server{Addr: *listenAddress}
logger.Log(web.ListenAndServe(server, *configFile, logger)) logger.Log(web.ListenAndServe(server, *webConfigFile, logger))
} }