prometheus-dnssec-exporter/main.go

230 lines
4.5 KiB
Go
Raw Normal View History

2018-10-04 10:36:49 +00:00
package main
import (
"flag"
"fmt"
"log"
"net/http"
"os"
"sync"
"time"
"github.com/miekg/dns"
"github.com/naoina/toml"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var addr = flag.String("listen-address", ":9204", "Prometheus metrics port")
var conf = flag.String("config", "/etc/dnssec-checks", "Configuration file")
var resolver = flag.String("resolver", "8.8.8.8:53", "Resolver to use")
var timeout = flag.Duration("timeout", 10*time.Second, "Timeout for network operations")
var dnsClient *dns.Client
type Records struct {
Zone string
Record string
Type string
}
type Exporter struct {
Records []Records
records *prometheus.GaugeVec
valid *prometheus.GaugeVec
}
func NewDNSSECExporter() *Exporter {
return &Exporter{
records: prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: "dnssec",
Subsystem: "zone",
Name: "record_days_left",
Help: "Number of days the signature will be valid",
},
[]string{
"zone",
"record",
"type",
},
),
valid: prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: "dnssec",
Subsystem: "zone",
Name: "record_valid",
Help: "Does this record pass DNSSEC validation",
},
[]string{
"zone",
"record",
"type",
},
),
}
}
func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
e.records.Describe(ch)
e.valid.Describe(ch)
}
func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
var wg sync.WaitGroup
wg.Add(len(e.Records))
for _, rec := range e.Records {
rec := rec
go func() {
e.collectRecord(rec.Zone, rec.Record, rec.Type)
wg.Done()
}()
}
wg.Wait()
e.records.Collect(ch)
e.valid.Collect(ch)
}
func (e *Exporter) collectRecord(zone, record, recordType string) {
// Start by finding the DNSKEY
msg := &dns.Msg{}
msg.SetQuestion(fmt.Sprintf("%s.", zone), dns.TypeDNSKEY)
response, _, err := dnsClient.Exchange(msg, *resolver)
if err != nil {
log.Printf("while looking up DNSKEY for %v: %v", zone, err)
return
}
// Found keys are mapped by tag -> key
keys := make(map[uint16]*dns.DNSKEY)
for _, rr := range response.Answer {
if dnskey, ok := rr.(*dns.DNSKEY); ok && dnskey.Flags&dns.ZONE != 0 {
keys[dnskey.KeyTag()] = dnskey
}
}
if len(keys) == 0 {
log.Printf("didn't find DNSKEY for %v", zone)
}
// Now lookup the signature
msg = &dns.Msg{}
msg.SetQuestion(hostname(zone, record), dns.TypeRRSIG)
response, _, err = dnsClient.Exchange(msg, *resolver)
if err != nil {
log.Printf("while looking up RRSIG for %v: %v", hostname(zone, record), err)
return
}
var sig *dns.RRSIG
var key *dns.DNSKEY
for _, rr := range response.Answer {
if rrsig, ok := rr.(*dns.RRSIG); ok {
if rrsig.TypeCovered == dns.StringToType[recordType] &&
keys[rrsig.KeyTag] != nil {
sig = rrsig
key = keys[rrsig.KeyTag]
break
}
}
}
if sig == nil {
log.Printf("didn't find RRSIG for %v covering type %v", hostname(zone, record), recordType)
return
}
exp := time.Unix(int64(sig.Expiration), 0)
if exp.IsZero() {
log.Print("zero exp")
return
}
e.records.WithLabelValues(
zone, record, recordType,
).Set(float64(time.Until(exp)/time.Hour) / 24)
// Finally, lookup the records to validate
if key == nil {
e.valid.WithLabelValues(zone, record, recordType).Set(0)
return
}
msg = &dns.Msg{}
msg.SetQuestion(hostname(zone, record), dns.StringToType[recordType])
response, _, err = dnsClient.Exchange(msg, *resolver)
if err != nil {
log.Printf("while looking up RRSet for %v type %v: %v", hostname(zone, record), recordType, err)
return
}
if err := sig.Verify(key, response.Answer); err == nil {
e.valid.WithLabelValues(zone, record, recordType).Set(1)
} else {
log.Printf("verify error for %v type %v): %v", hostname(zone, record), recordType, err)
e.valid.WithLabelValues(zone, record, recordType).Set(0)
}
}
func hostname(zone, record string) string {
if record == "@" {
return fmt.Sprintf("%s.", zone)
}
return fmt.Sprintf("%s.%s.", record, zone)
}
func main() {
flag.Parse()
dnsClient = &dns.Client{
Net: "tcp",
Timeout: *timeout,
}
f, err := os.Open(*conf)
if err != nil {
log.Fatalf("couldn't open configuration file: %v", err)
}
exporter := NewDNSSECExporter()
if err := toml.NewDecoder(f).Decode(exporter); err != nil {
log.Fatalf("couldn't parse configuration file: %v", err)
}
prometheus.MustRegister(exporter)
http.Handle("/metrics", promhttp.Handler())
log.Fatal(http.ListenAndServe(*addr, nil))
}