Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for domain based blacklists #194

Merged
merged 2 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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..
Expand Down
14 changes: 10 additions & 4 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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()
Expand All @@ -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)

Expand Down
30 changes: 21 additions & 9 deletions collector/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type RblCollector struct {
rbls []string
util *dns.DNSUtil
targets []string
domainBased bool
logger *slog.Logger
}

Expand All @@ -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"),
Expand Down Expand Up @@ -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,
}
}

Expand Down Expand Up @@ -120,7 +122,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 {
Expand All @@ -147,9 +156,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 {
Expand Down
123 changes: 81 additions & 42 deletions collector/collector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,50 +10,89 @@ import (
"github.com/stretchr/testify/assert"
)

func TestCollector(t *testing.T) {
func TestCollectorSuite(t *testing.T) {
dnsMock := tests.CreateDNSMock(t)
defer dnsMock.Close()

logger := tests.CreateTestLogger(t)
util := tests.CreateDNSUtil(t, dnsMock.LocalAddr())
rbls := []string{"zen.spamhaus.org", "cbl.abuseat.org"}
targets := []string{
"79.214.198.85", // bad
"relay.heise.de", // good
"1.3.3.7", // good
}

c := collector.NewRblCollector(rbls, targets, util, logger)

result, err := testutil.CollectAndLint(c)
assert.Empty(t, result)
assert.NoError(t, err)

// take all metrics but duration as it's value is hardly predictable
metrics := []string{}
for _, metric := range []string{"used", "ips_blacklisted", "errors", "listed", "targets"} {
metrics = append(metrics, collector.BuildFQName(metric))
}
expected := `
# HELP luzilla_rbls_ips_blacklisted Blacklisted IPs
# TYPE luzilla_rbls_ips_blacklisted gauge
luzilla_rbls_ips_blacklisted{hostname="1.3.3.7",ip="1.3.3.7",rbl="cbl.abuseat.org"} 0
luzilla_rbls_ips_blacklisted{hostname="1.3.3.7",ip="1.3.3.7",rbl="zen.spamhaus.org"} 0
luzilla_rbls_ips_blacklisted{hostname="79.214.198.85",ip="79.214.198.85",rbl="cbl.abuseat.org"} 0
luzilla_rbls_ips_blacklisted{hostname="79.214.198.85",ip="79.214.198.85",rbl="zen.spamhaus.org"} 1
luzilla_rbls_ips_blacklisted{hostname="relay.heise.de",ip="193.99.145.50",rbl="cbl.abuseat.org"} 0
luzilla_rbls_ips_blacklisted{hostname="relay.heise.de",ip="193.99.145.50",rbl="zen.spamhaus.org"} 0
# HELP luzilla_rbls_listed The number of listings in RBLs (this is bad)
# TYPE luzilla_rbls_listed gauge
luzilla_rbls_listed{rbl="cbl.abuseat.org"} 0
luzilla_rbls_listed{rbl="zen.spamhaus.org"} 1
# HELP luzilla_rbls_targets The number of targets that are being probed (configured via targets.ini or ?target=)
# TYPE luzilla_rbls_targets gauge
luzilla_rbls_targets 3
# HELP luzilla_rbls_used The number of RBLs to check IPs against (configured via rbls.ini)
# TYPE luzilla_rbls_used gauge
luzilla_rbls_used 2
`
err = testutil.CollectAndCompare(c, strings.NewReader(expected), metrics...)
assert.NoError(t, err)

t.Run("test=ip-based", func(t *testing.T) {
rbls := []string{"zen.spamhaus.org", "cbl.abuseat.org"}
targets := []string{
"79.214.198.85", // bad
"relay.heise.de", // good
"1.3.3.7", // good
}

c := collector.NewRblCollector(rbls, targets, false, util, logger)

result, err := testutil.CollectAndLint(c)
assert.Empty(t, result)
assert.NoError(t, err)

// take all metrics but duration as it's value is hardly predictable
metrics := []string{}
for _, metric := range []string{"used", "ips_blacklisted", "errors", "listed", "targets"} {
metrics = append(metrics, collector.BuildFQName(metric))
}
expected := `
# HELP luzilla_rbls_ips_blacklisted Blacklisted IPs
# TYPE luzilla_rbls_ips_blacklisted gauge
luzilla_rbls_ips_blacklisted{hostname="1.3.3.7",ip="1.3.3.7",rbl="cbl.abuseat.org"} 0
luzilla_rbls_ips_blacklisted{hostname="1.3.3.7",ip="1.3.3.7",rbl="zen.spamhaus.org"} 0
luzilla_rbls_ips_blacklisted{hostname="79.214.198.85",ip="79.214.198.85",rbl="cbl.abuseat.org"} 0
luzilla_rbls_ips_blacklisted{hostname="79.214.198.85",ip="79.214.198.85",rbl="zen.spamhaus.org"} 1
luzilla_rbls_ips_blacklisted{hostname="relay.heise.de",ip="193.99.145.50",rbl="cbl.abuseat.org"} 0
luzilla_rbls_ips_blacklisted{hostname="relay.heise.de",ip="193.99.145.50",rbl="zen.spamhaus.org"} 0
# HELP luzilla_rbls_listed The number of listings in RBLs (this is bad)
# TYPE luzilla_rbls_listed gauge
luzilla_rbls_listed{rbl="cbl.abuseat.org"} 0
luzilla_rbls_listed{rbl="zen.spamhaus.org"} 1
# HELP luzilla_rbls_targets The number of targets that are being probed (configured via targets.ini or ?target=)
# TYPE luzilla_rbls_targets gauge
luzilla_rbls_targets 3
# HELP luzilla_rbls_used The number of RBLs to check IPs against (configured via rbls.ini)
# TYPE luzilla_rbls_used gauge
luzilla_rbls_used 2
`
err = testutil.CollectAndCompare(c, strings.NewReader(expected), metrics...)
assert.NoError(t, err)
})

t.Run("test=domain-based", func(t *testing.T) {
rbls := []string{"dbl.spamhaus.org"}
targets := []string{
"dbltest.com", // bad
"example.com", // good
}

c := collector.NewRblCollector(rbls, targets, true, util, logger)

result, err := testutil.CollectAndLint(c)
assert.Empty(t, result)
assert.NoError(t, err)

// take all metrics but duration as it's value is hardly predictable
metrics := []string{}
for _, metric := range []string{"used", "ips_blacklisted", "errors", "listed", "targets"} {
metrics = append(metrics, collector.BuildFQName(metric))
}
expected := `
# HELP luzilla_rbls_ips_blacklisted Blacklisted IPs
# TYPE luzilla_rbls_ips_blacklisted gauge
luzilla_rbls_ips_blacklisted{hostname="dbltest.com",ip="127.0.1.2",rbl="dbl.spamhaus.org"} 1
luzilla_rbls_ips_blacklisted{hostname="example.com",ip="",rbl="dbl.spamhaus.org"} 0
# HELP luzilla_rbls_listed The number of listings in RBLs (this is bad)
# TYPE luzilla_rbls_listed gauge
luzilla_rbls_listed{rbl="dbl.spamhaus.org"} 1
# HELP luzilla_rbls_targets The number of targets that are being probed (configured via targets.ini or ?target=)
# TYPE luzilla_rbls_targets gauge
luzilla_rbls_targets 2
# HELP luzilla_rbls_used The number of RBLs to check IPs against (configured via rbls.ini)
# TYPE luzilla_rbls_used gauge
luzilla_rbls_used 1
`
err = testutil.CollectAndCompare(c, strings.NewReader(expected), metrics...)
assert.NoError(t, err)
})
}
9 changes: 5 additions & 4 deletions internal/prober/prober.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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{
Expand Down
4 changes: 2 additions & 2 deletions internal/setup/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 7 additions & 0 deletions internal/tests/tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
28 changes: 22 additions & 6 deletions pkg/rbl/rbl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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

Expand All @@ -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()))

Expand Down
Loading