diff --git a/.gitignore b/.gitignore index d87977c..b953c86 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ prometheus-dnssec-exporter +config.yaml diff --git a/go.mod b/go.mod index 4ff4518..f22040c 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/prometheus/common v0.29.0 github.com/prometheus/exporter-toolkit v0.7.1 gopkg.in/alecthomas/kingpin.v2 v2.2.6 + gopkg.in/yaml.v2 v2.4.0 ) require ( @@ -35,5 +36,4 @@ require ( golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/appengine v1.6.6 // indirect google.golang.org/protobuf v1.26.0-rc.1 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/main.go b/main.go index 3a9a0e2..e034b85 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "io/ioutil" "net/http" "os" "sort" @@ -17,28 +18,42 @@ import ( "github.com/prometheus/common/version" "github.com/prometheus/exporter-toolkit/web" kingpin "gopkg.in/alecthomas/kingpin.v2" + "gopkg.in/yaml.v2" ) const namespace = "dnssec" -var removalCds = DsData{ - KeyTag: 0, - Algorithm: 0, - DigestType: 0, - Digest: "00", -} - type MetricsCollector struct { Log log.Logger signatureOkMetric *prom.GaugeVec - dsPresentMetric *prom.GaugeVec - cdsPresentMetric *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{ @@ -48,19 +63,43 @@ func NewMetricsCollector(zonelist []string, resolver string, logger log.Logger) }, []string{"zone", "tld", "parent"}, ) - dsPresentMetric := prom.NewGaugeVec( + sokRcodeMetric := prom.NewGaugeVec( prom.GaugeOpts{ Namespace: namespace, - Name: "ds_present", - Help: "1 if CDS record is present in the parent zone, 0 otherwise", + Name: "signature_rcode", + Help: "RCode of the DNS query", }, []string{"zone", "tld", "parent"}, ) - cdsPresentMetric := prom.NewGaugeVec( + dsCountMetric := prom.NewGaugeVec( prom.GaugeOpts{ Namespace: namespace, - Name: "cds_present", - Help: "1 if a CDS record is present in the zone, 0 otherwise", + 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"}, ) @@ -74,8 +113,11 @@ func NewMetricsCollector(zonelist []string, resolver string, logger log.Logger) ) collector := MetricsCollector{ signatureOkMetric: signatureOkMetric, - dsPresentMetric: dsPresentMetric, - cdsPresentMetric: cdsPresentMetric, + sokRcodeMetric: sokRcodeMetric, + dsCountMetric: dsCountMetric, + dsRcodeMetric: dsRcodeMetric, + cdsCountMetric: cdsCountMetric, + cdsRcodeMetric: cdsRcodeMetric, cdsDsMatchMetric: cdsDsMatchMetric, zones: zonelist, Resolver: resolver, @@ -86,29 +128,16 @@ func NewMetricsCollector(zonelist []string, resolver string, logger log.Logger) func (c *MetricsCollector) Describe(ch chan<- *prom.Desc) { c.signatureOkMetric.Describe(ch) - c.dsPresentMetric.Describe(ch) - c.cdsPresentMetric.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) { - 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 { - sok, err1 := c.signatureOk(zone) - if err1 != nil { - continue - } - dsp, cdp, cdm, err2 := c.cdsDsMatches(zone) - if err2 != nil { - continue - } labels := strings.Split(zone, ".") tld := strings.Join(labels[len(labels)-2:], ".") parent := strings.Join(labels[1:], ".") @@ -117,37 +146,23 @@ func (c *MetricsCollector) reportMetrics(ch chan<- prom.Metric) error { "tld": tld, "parent": parent, } - signatureOkMetric := c.signatureOkMetric.With(promlabels) - dsPresentMetric := c.dsPresentMetric.With(promlabels) - cdsPresentMetric := c.cdsPresentMetric.With(promlabels) - cdsDsMatchMetric := c.cdsDsMatchMetric.With(promlabels) - if sok { - signatureOkMetric.Set(1) - } else { - signatureOkMetric.Set(0) - } - 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) - } + 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.dsPresentMetric.Collect(ch) - c.cdsPresentMetric.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) - return nil } type DsData struct { @@ -169,69 +184,76 @@ func Equal(a, b []DsData) bool { return true } -func (c *MetricsCollector) signatureOk(zone string) (bool, error) { +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, err + 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, nil + return rsoa.MsgHdr.AuthenticatedData && rsoa.MsgHdr.Rcode == dns.RcodeSuccess, rsoa.MsgHdr.Rcode, nil } -func (c *MetricsCollector) cdsDsMatches(zone string) (bool, bool, bool, error) { - dsPresent := false - cdsPresent := false - +func (c *MetricsCollector) cdsDsMatches(zone string) (int, int, int, int, bool, error) { + dsCount := 0 + dsRcode := -1 + cdsCount := 0 + cdsRcode := -1 + match := false + var dsdata []DsData + var cdsdata []DsData + qcds := new(dns.Msg) qcds.SetQuestion(dns.Fqdn(zone), dns.TypeCDS) - rcds, _, err := c.DNS.Exchange(qcds, c.Resolver) - if err != nil { - return dsPresent, cdsPresent, false, err - } - cdsPresent = len(rcds.Answer) > 0 - 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, + 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 }) } - 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, _, err := c.DNS.Exchange(qds, c.Resolver) - if err != nil { - return dsPresent, cdsPresent, false, err + 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 }) } - dsPresent = len(rds.Answer) > 0 - 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, + + 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 } } - sort.SliceStable(dsdata, func(a, b int) bool { return dsdata[a].KeyTag < dsdata[b].KeyTag }) - - 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 + return dsCount, dsRcode, cdsCount, cdsRcode, match, nil } func main() { @@ -244,17 +266,13 @@ func main() { "web.metrics-path", "Path under which to expose metrics.", ).Default("/metrics").String() - resolver = 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( + webConfigFile = kingpin.Flag( "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() ) @@ -264,19 +282,38 @@ func main() { kingpin.CommandLine.UsageWriter(os.Stdout) kingpin.HelpFlag.Short('h') 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 = 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) http.Handle(*metricsPath, promhttp.Handler()) server := &http.Server{Addr: *listenAddress} - logger.Log(web.ListenAndServe(server, *configFile, logger)) + logger.Log(web.ListenAndServe(server, *webConfigFile, logger)) }