diff --git a/Makefile b/Makefile index f0fb6ef..a35dab5 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,14 @@ run-dev: --log.debug \ --config.dns-resolver 0.0.0.0:15353 +.PHONY: run-dev-domain +run-dev-domain: + go run dnsbl_exporter.go \ + --log.debug \ + --config.rbls ./rbls-domain.ini \ + --config.domain-based \ + --config.dns-resolver 0.0.0.0:15353 + .PHONY: snapshot snapshot: goreleaser build --snapshot --clean diff --git a/README.md b/README.md index 5d121d7..c6cb8de 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ $ dnsbl-exporter -h --config.dns-resolver value IP address of the resolver to use. (default: "127.0.0.1:53") --config.rbls value Configuration file which contains RBLs (default: "./rbls.ini") --config.targets value Configuration file which contains the targets to check. (default: "./targets.ini") +--config.domain-based RBLS are domain instead of IP based blacklists (default: false) --web.listen-address value Address to listen on for web interface and telemetry. (default: ":9211") --web.telemetry-path value Path under which to expose metrics. (default: "/metrics") --log.debug Enable more output in the logs, otherwise INFO. @@ -85,6 +86,8 @@ luzilla_rbls_ips_blacklisted{hostname="mail.gmx.net",ip="212.227.17.168",rbl="ix This represent the server's hostname and the DNSBL in question. `0` for unlisted and `1` for listed. Requests to the DNSBL happen in real-time and are not cached. Take this into account and use accordingly. +If the exporter is configured for DNS based blacklists, the ip label represents the return code of the blacklist. + ### Caveat In order to use this, a _proper_ DNS resolver is needed. Proper means: not Google, not Cloudflare, OpenDNS, etc.. diff --git a/app/app.go b/app/app.go index 6631c98..486ed73 100644 --- a/app/app.go +++ b/app/app.go @@ -58,6 +58,11 @@ func NewApp(name string, version string) DNSBLApp { Usage: "Configuration file which contains the targets to check.", EnvVars: []string{"DNSBL_EXP_TARGETS"}, }, + &cli.BoolFlag{ + Name: "config.domain-based", + Usage: "RBLS are domain based blacklists.", + Value: false, + }, &cli.StringFlag{ Name: "web.listen-address", Value: ":9211", @@ -169,7 +174,7 @@ func (a *DNSBLApp) Bootstrap() { return err } - rblCollector := setup.CreateCollector(rbls, targets, dnsUtil, log.With("area", "metrics")) + rblCollector := setup.CreateCollector(rbls, targets, ctx.Bool("config.domain-based"), dnsUtil, log.With("area", "metrics")) registry.MustRegister(rblCollector) registryExporter := setup.CreateRegistry() @@ -191,9 +196,10 @@ func (a *DNSBLApp) Bootstrap() { http.Handle(ctx.String("web.telemetry-path"), mHandler.Handler()) pHandler := prober.ProberHandler{ - DNS: dnsUtil, - Rbls: rbls, - Logger: log.With("area", "prober"), + DNS: dnsUtil, + Rbls: rbls, + DomainBased: ctx.Bool("config.domain-based"), + Logger: log.With("area", "prober"), } http.Handle("/prober", pHandler) diff --git a/collector/collector.go b/collector/collector.go index 491145f..3aade20 100644 --- a/collector/collector.go +++ b/collector/collector.go @@ -24,6 +24,7 @@ type RblCollector struct { rbls []string util *dns.DNSUtil targets []string + domainBased bool logger *slog.Logger } @@ -32,7 +33,7 @@ func BuildFQName(metric string) string { } // NewRblCollector ... creates the collector -func NewRblCollector(rbls []string, targets []string, util *dns.DNSUtil, logger *slog.Logger) *RblCollector { +func NewRblCollector(rbls []string, targets []string, domainBased bool, util *dns.DNSUtil, logger *slog.Logger) *RblCollector { return &RblCollector{ configuredMetric: prometheus.NewDesc( BuildFQName("used"), @@ -70,10 +71,11 @@ func NewRblCollector(rbls []string, targets []string, util *dns.DNSUtil, logger nil, nil, ), - rbls: rbls, - util: util, - targets: targets, - logger: logger, + rbls: rbls, + util: util, + targets: targets, + domainBased: domainBased, + logger: logger, } } @@ -116,7 +118,14 @@ func (c *RblCollector) Collect(ch chan<- prometheus.Metric) { close(targets) }() for _, host := range hosts { - go resolver.Do(host, targets, wg.Done) + if c.domainBased { + go func(hostname string) { + targets <- rbl.Target{Host: hostname} + wg.Done() + }(host) + } else { + go resolver.Do(host, targets, wg.Done) + } } // run the check for target := range targets { @@ -143,9 +152,12 @@ func (c *RblCollector) Collect(ch chan<- prometheus.Metric) { listed.Store(check.Rbl, val.(int)+1) } - c.logger.Debug("listed?", slog.Int("v", metricValue), slog.String("rbl", check.Rbl)) - - labelValues := []string{check.Rbl, check.Target.IP.String(), check.Target.Host} + c.logger.Debug("listed?", slog.Int("v", metricValue), slog.String("rbl", check.Rbl), slog.String("reason", check.Text)) + ip := "" + if len(check.Target.IP) > 0 { + ip = check.Target.IP.String() + } + labelValues := []string{check.Rbl, ip, check.Target.Host} // this is an "error" from the RBL/transport if check.Error { diff --git a/internal/prober/prober.go b/internal/prober/prober.go index 28268e6..a2b049c 100644 --- a/internal/prober/prober.go +++ b/internal/prober/prober.go @@ -10,9 +10,10 @@ import ( ) type ProberHandler struct { - DNS *dns.DNSUtil - Rbls []string - Logger *slog.Logger + DNS *dns.DNSUtil + Rbls []string + DomainBased bool + Logger *slog.Logger } func (p ProberHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -26,7 +27,7 @@ func (p ProberHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { targets = append(targets, r.URL.Query().Get("target")) registry := setup.CreateRegistry() - collector := setup.CreateCollector(p.Rbls, targets, p.DNS, p.Logger) + collector := setup.CreateCollector(p.Rbls, targets, p.DomainBased, p.DNS, p.Logger) registry.MustRegister(collector) h := promhttp.HandlerFor(registry, promhttp.HandlerOpts{ diff --git a/internal/setup/setup.go b/internal/setup/setup.go index 147eae7..3d315f7 100644 --- a/internal/setup/setup.go +++ b/internal/setup/setup.go @@ -7,8 +7,8 @@ import ( "golang.org/x/exp/slog" ) -func CreateCollector(rbls []string, targets []string, dnsUtil *dns.DNSUtil, logger *slog.Logger) *collector.RblCollector { - return collector.NewRblCollector(rbls, targets, dnsUtil, logger) +func CreateCollector(rbls []string, targets []string, domainBased bool, dnsUtil *dns.DNSUtil, logger *slog.Logger) *collector.RblCollector { + return collector.NewRblCollector(rbls, targets, domainBased, dnsUtil, logger) } func CreateRegistry() *prometheus.Registry { diff --git a/internal/tests/tests.go b/internal/tests/tests.go index 4a2c000..67ea243 100644 --- a/internal/tests/tests.go +++ b/internal/tests/tests.go @@ -54,6 +54,13 @@ func CreateDNSMock(t *testing.T) *mockdns.Server { "10.0.0.127.zen.spamhaus.org.": { TXT: []string{"https://www.spamhaus.org/query/ip/127.0.0.10"}, }, + // domain based rbl responses + // https://www.spamhaus.org/faq/section/Spamhaus%20DBL#277 + "dbltest.com.dbl.spamhaus.org.": { + A: []string{"127.0.1.2"}, + TXT: []string{"https://www.spamhaus.org/query/domain/dbltest.com"}, + }, + "example.com.dbl.spamhaus.org.": {}, }, true) if err != nil { assert.FailNow(t, "failed building mock", "error: %s", err) diff --git a/pkg/rbl/rbl.go b/pkg/rbl/rbl.go index abd04c2..09d94f3 100644 --- a/pkg/rbl/rbl.go +++ b/pkg/rbl/rbl.go @@ -44,18 +44,25 @@ func (rbl *RBL) Update(target Target, blocklist string, c chan<- Result) { slog.String("rbl", blocklist)))) } -func (r *RBL) lookup(blocklist string, ip Target, c chan<- Result, logger *slog.Logger) { +func (r *RBL) lookup(blocklist string, target Target, c chan<- Result, logger *slog.Logger) { logger.Debug("next up") result := Result{ - Target: ip, + Target: target, Listed: false, Rbl: blocklist, } + domainBased := target.IP == nil logger.Debug("about to query RBL") - lookup := godnsbl.Reverse(ip.IP) + "." + result.Rbl + var lookup string + if domainBased { + lookup = target.Host + "." + result.Rbl + } else { + lookup = godnsbl.Reverse(target.IP) + "." + result.Rbl + } + logger.Debug("built lookup", slog.String("lookup", lookup)) res, err := r.util.GetARecords(lookup) @@ -69,12 +76,12 @@ func (r *RBL) lookup(blocklist string, ip Target, c chan<- Result, logger *slog. } if len(res) == 0 { - // ip is not listed + // target (domain or ip) is not listed c <- result return } - logger.Debug("ip is listed") + logger.Debug("target is listed") result.Listed = true @@ -86,9 +93,18 @@ func (r *RBL) lookup(blocklist string, ip Target, c chan<- Result, logger *slog. c <- result return } + if domainBased { + // @see https://datatracker.ietf.org/doc/html/rfc5782 + result.Target.IP = reason + } // fetch (potential) reason - txt, err := r.util.GetTxtRecords(godnsbl.Reverse(reason) + "." + result.Rbl) + var txt []string + if domainBased { + txt, err = r.util.GetTxtRecords(lookup) + } else { + txt, err = r.util.GetTxtRecords(godnsbl.Reverse(reason) + "." + result.Rbl) + } if err != nil { logger.Error("error occurred fetching TXT record", slog.String("msg", err.Error())) diff --git a/pkg/rbl/rbl_test.go b/pkg/rbl/rbl_test.go index 8463d15..2b6c8dd 100644 --- a/pkg/rbl/rbl_test.go +++ b/pkg/rbl/rbl_test.go @@ -112,6 +112,41 @@ func TestRblSuite(t *testing.T) { assert.True(t, result.Listed) assert.Contains(t, result.Text, "https://www.spamhaus.org/") }) + + t.Run("run=domain", func(t *testing.T) { + dnsMock := tests.CreateDNSMock(t) + defer dnsMock.Close() + + logger := tests.CreateTestLogger(t) + d := tests.CreateDNSUtil(t, dnsMock.LocalAddr()) + r := rbl.New(d, logger) + + // https://www.spamhaus.org/faq/section/Spamhaus%20DBL#277 + targets := []rbl.Target{ + {Host: "dbltest.com"}, + {Host: "example.com"}, + } + + for _, target := range targets { + blocklist := "dbl.spamhaus.org" + c := make(chan rbl.Result) + defer close(c) + + r.Update(target, blocklist, c) + + res := <-c + assert.False(t, res.Error) + assert.NoError(t, res.ErrorType) + if target.Host == "dbltest.com" { + assert.True(t, res.Listed) + assert.Equal(t, "127.0.1.2", res.Target.IP.String()) + assert.Equal(t, res.Text, "https://www.spamhaus.org/query/domain/dbltest.com") + } else { + assert.False(t, res.Listed) + assert.Nil(t, res.Target.IP) + } + } + }) } func TestResolver(t *testing.T) { diff --git a/rbls-domain.ini b/rbls-domain.ini new file mode 100644 index 0000000..05f598b --- /dev/null +++ b/rbls-domain.ini @@ -0,0 +1,2 @@ +[rbl] +server=dbl.spamhaus.org diff --git a/targets.ini b/targets.ini index 0287d85..54204bd 100644 --- a/targets.ini +++ b/targets.ini @@ -6,3 +6,4 @@ server=smtp.gmail.com server=mail.gmx.net server=smtp.yahoo.com server=smtp.fastmail.com +server=dbltest.com