2018-10-04 10:36:49 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"flag"
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
2018-11-10 14:18:26 +00:00
|
|
|
"strings"
|
2018-10-04 10:36:49 +00:00
|
|
|
"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")
|
2018-11-10 14:18:26 +00:00
|
|
|
var resolvers = flag.String("resolvers", "8.8.8.8:53,1.1.1.1:53", "Resolvers to use (comma separated)")
|
2018-10-04 10:36:49 +00:00
|
|
|
var timeout = flag.Duration("timeout", 10*time.Second, "Timeout for network operations")
|
|
|
|
|
|
|
|
type Records struct {
|
|
|
|
Zone string
|
|
|
|
Record string
|
|
|
|
Type string
|
|
|
|
}
|
|
|
|
|
2018-10-05 09:47:11 +00:00
|
|
|
type Logger interface {
|
|
|
|
Print(v ...interface{})
|
|
|
|
Printf(format string, v ...interface{})
|
|
|
|
}
|
|
|
|
|
2018-10-04 10:36:49 +00:00
|
|
|
type Exporter struct {
|
|
|
|
Records []Records
|
|
|
|
|
2018-11-10 14:18:26 +00:00
|
|
|
records *prometheus.GaugeVec
|
|
|
|
resolves *prometheus.GaugeVec
|
2022-09-24 23:52:08 +00:00
|
|
|
expiry *prometheus.GaugeVec
|
2018-10-05 09:47:11 +00:00
|
|
|
|
2018-11-10 14:18:26 +00:00
|
|
|
resolvers []string
|
2018-10-05 09:47:11 +00:00
|
|
|
dnsClient *dns.Client
|
|
|
|
|
|
|
|
logger Logger
|
2018-10-04 10:36:49 +00:00
|
|
|
}
|
|
|
|
|
2018-11-10 14:18:26 +00:00
|
|
|
func NewDNSSECExporter(timeout time.Duration, resolvers []string, logger Logger) *Exporter {
|
2018-10-04 10:36:49 +00:00
|
|
|
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",
|
|
|
|
},
|
|
|
|
),
|
2018-11-10 14:18:26 +00:00
|
|
|
resolves: prometheus.NewGaugeVec(
|
2018-10-04 10:36:49 +00:00
|
|
|
prometheus.GaugeOpts{
|
|
|
|
Namespace: "dnssec",
|
|
|
|
Subsystem: "zone",
|
2018-11-10 14:18:26 +00:00
|
|
|
Name: "record_resolves",
|
|
|
|
Help: "Does the record resolve using the specified DNSSEC enabled resolvers",
|
2018-10-04 10:36:49 +00:00
|
|
|
},
|
|
|
|
[]string{
|
2018-11-10 14:18:26 +00:00
|
|
|
"resolver",
|
2018-10-04 10:36:49 +00:00
|
|
|
"zone",
|
|
|
|
"record",
|
|
|
|
"type",
|
|
|
|
},
|
|
|
|
),
|
2022-09-24 23:52:08 +00:00
|
|
|
expiry: prometheus.NewGaugeVec(
|
|
|
|
prometheus.GaugeOpts{
|
|
|
|
Namespace: "dnssec",
|
|
|
|
Subsystem: "zone",
|
|
|
|
Name: "record_earliest_rrsig_expiry",
|
|
|
|
Help: "Earliest expiring RRSIG covering the record on resolver in unixtime",
|
|
|
|
},
|
|
|
|
[]string{
|
|
|
|
"resolver",
|
|
|
|
"zone",
|
|
|
|
"record",
|
|
|
|
"type",
|
|
|
|
},
|
|
|
|
),
|
2018-10-05 10:58:14 +00:00
|
|
|
dnsClient: &dns.Client{
|
|
|
|
Net: "tcp",
|
|
|
|
Timeout: timeout,
|
|
|
|
},
|
2018-11-10 14:18:26 +00:00
|
|
|
resolvers: resolvers,
|
|
|
|
logger: logger,
|
2018-10-04 10:36:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
|
|
|
|
e.records.Describe(ch)
|
2018-11-10 14:18:26 +00:00
|
|
|
e.resolves.Describe(ch)
|
2022-09-24 23:52:08 +00:00
|
|
|
e.expiry.Describe(ch)
|
2018-10-04 10:36:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
|
|
|
|
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
|
2022-09-24 23:52:08 +00:00
|
|
|
wg.Add(len(e.Records) * (len(e.resolvers)))
|
2018-10-04 10:36:49 +00:00
|
|
|
|
|
|
|
for _, rec := range e.Records {
|
|
|
|
|
|
|
|
rec := rec
|
|
|
|
|
2018-11-10 14:18:26 +00:00
|
|
|
// Check the configured resolvers
|
2018-10-04 10:36:49 +00:00
|
|
|
|
2018-11-10 14:18:26 +00:00
|
|
|
for _, resolver := range e.resolvers {
|
2018-10-04 10:36:49 +00:00
|
|
|
|
2018-11-10 14:18:26 +00:00
|
|
|
resolver := resolver
|
2018-10-04 10:36:49 +00:00
|
|
|
|
2018-11-10 14:18:26 +00:00
|
|
|
go func() {
|
2018-10-04 10:36:49 +00:00
|
|
|
|
2022-09-24 23:52:08 +00:00
|
|
|
resolves, expires := e.resolve(rec.Zone, rec.Record, rec.Type, resolver)
|
2018-10-04 10:36:49 +00:00
|
|
|
|
2018-11-10 14:18:26 +00:00
|
|
|
e.resolves.WithLabelValues(
|
|
|
|
resolver, rec.Zone, rec.Record, rec.Type,
|
|
|
|
).Set(map[bool]float64{true: 1}[resolves])
|
2018-10-04 10:36:49 +00:00
|
|
|
|
2022-09-24 23:52:08 +00:00
|
|
|
// Only return the signature expiry if the record resolves.
|
|
|
|
if resolves {
|
|
|
|
e.expiry.WithLabelValues(
|
|
|
|
resolver, rec.Zone, rec.Record, rec.Type,
|
|
|
|
).Set(float64(expires.Unix()))
|
|
|
|
}
|
|
|
|
|
|
|
|
// For compatibility with historical behaviour, record_days_left
|
|
|
|
// returns the time until the earliest RRSIG expiration on the
|
|
|
|
// first configured resolver. This value will be bogus if that
|
|
|
|
// resolver fails to resolve and validate the record.
|
|
|
|
if (resolver == e.resolvers[0]) {
|
|
|
|
e.records.WithLabelValues(
|
|
|
|
rec.Zone, rec.Record, rec.Type,
|
|
|
|
).Set(float64(time.Until(expires)/time.Hour) / 24)
|
|
|
|
}
|
|
|
|
|
2018-11-10 14:18:26 +00:00
|
|
|
wg.Done()
|
2018-10-04 10:36:49 +00:00
|
|
|
|
2018-11-10 14:18:26 +00:00
|
|
|
}()
|
2018-10-04 10:36:49 +00:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2018-11-10 14:18:26 +00:00
|
|
|
wg.Wait()
|
2018-10-04 10:36:49 +00:00
|
|
|
|
2018-11-10 14:18:26 +00:00
|
|
|
e.records.Collect(ch)
|
|
|
|
e.resolves.Collect(ch)
|
2022-09-24 23:52:08 +00:00
|
|
|
e.expiry.Collect(ch)
|
2018-11-10 14:18:26 +00:00
|
|
|
|
|
|
|
}
|
|
|
|
|
2022-09-24 23:52:08 +00:00
|
|
|
func (e *Exporter) resolve(zone, record, recordType, resolver string) (resolves bool, expires time.Time) {
|
2018-11-10 14:18:26 +00:00
|
|
|
|
|
|
|
msg := &dns.Msg{}
|
2018-10-04 10:36:49 +00:00
|
|
|
msg.SetQuestion(hostname(zone, record), dns.StringToType[recordType])
|
2018-11-10 14:18:26 +00:00
|
|
|
msg.SetEdns0(4096, true)
|
2018-10-04 10:36:49 +00:00
|
|
|
|
2018-11-10 14:18:26 +00:00
|
|
|
response, _, err := e.dnsClient.Exchange(msg, resolver)
|
2018-10-04 10:36:49 +00:00
|
|
|
if err != nil {
|
2022-09-24 23:52:08 +00:00
|
|
|
e.logger.Printf("error resolving %v %v on %v: %v", hostname(zone, record), recordType, resolver, err)
|
2018-10-04 10:36:49 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-09-24 23:52:08 +00:00
|
|
|
resolves = response.AuthenticatedData &&
|
2018-11-10 14:18:26 +00:00
|
|
|
!response.CheckingDisabled &&
|
|
|
|
response.Rcode == dns.RcodeSuccess
|
2018-10-05 09:47:11 +00:00
|
|
|
|
2022-09-24 23:52:08 +00:00
|
|
|
// If multiple RRSIGs cover our record, return the one that will expire the earliest.
|
|
|
|
for _, rr := range response.Answer {
|
|
|
|
if rrsig, ok := rr.(*dns.RRSIG); ok {
|
|
|
|
sigexp := time.Unix(int64(rrsig.Expiration), 0)
|
|
|
|
if (expires.IsZero() || sigexp.Before(expires) && !sigexp.IsZero()) {
|
|
|
|
expires = sigexp;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
2018-10-04 10:36:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func hostname(zone, record string) string {
|
|
|
|
|
|
|
|
if record == "@" {
|
|
|
|
return fmt.Sprintf("%s.", zone)
|
|
|
|
}
|
|
|
|
|
|
|
|
return fmt.Sprintf("%s.%s.", record, zone)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
|
|
|
flag.Parse()
|
|
|
|
|
|
|
|
f, err := os.Open(*conf)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("couldn't open configuration file: %v", err)
|
|
|
|
}
|
|
|
|
|
2018-10-05 09:47:11 +00:00
|
|
|
logger := log.New(os.Stderr, "", log.LstdFlags)
|
|
|
|
|
2018-11-10 14:18:26 +00:00
|
|
|
r := strings.Split(*resolvers, ",")
|
|
|
|
|
|
|
|
exporter := NewDNSSECExporter(*timeout, r, logger)
|
2018-10-04 10:36:49 +00:00
|
|
|
|
|
|
|
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))
|
|
|
|
|
|
|
|
}
|