diff --git a/blackhole.go b/blackhole.go index afa9935..fc8608d 100644 --- a/blackhole.go +++ b/blackhole.go @@ -1,6 +1,25 @@ package pancheri -import "slices" +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "slices" +) + +var ( + blockedDomains = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "pancheri_blackhole_blocked_domains_total", + Help: "Number of blocked domains on the blackhole blocklist", + }) + blockHits = promauto.NewCounter(prometheus.CounterOpts{ + Name: "pancheri_blackhole_block_hits_total", + Help: "Number of requests that have hit the blocklist", + }) + blockMisses = promauto.NewCounter(prometheus.CounterOpts{ + Name: "pancheri_blackhole_misses_total", + Help: "Number of requests that have missed the blocklist", + }) +) type BlackholeFile struct { DenyDomains []string `yaml:"deny_domains"` @@ -11,5 +30,13 @@ type Blackholer struct { } func (b *Blackholer) ShouldBlock(domain string) bool { - return slices.Contains(b.DenyDomains, domain) + blockedDomains.Set(float64(len(b.DenyDomains))) + blocked := slices.Contains(b.DenyDomains, domain) + if blocked { + blockHits.Inc() + } else { + blockMisses.Inc() + } + + return blocked } diff --git a/cmd/pancheri/main.go b/cmd/pancheri/main.go index 66b214f..67bedd0 100644 --- a/cmd/pancheri/main.go +++ b/cmd/pancheri/main.go @@ -51,6 +51,18 @@ func main() { zones[authoritativeZone] = zone } + logrus.WithFields(logrus.Fields{ + "port": "2112", + }).Info("starting promhttp listener") + + go func() { + err := pancheri.PromMain() + if err != nil { + logrus.Errorf("error in prom listener: %s", err) + os.Exit(1) + } + }() + logrus.WithFields(logrus.Fields{ "host": c.Server.Host, "port": c.Server.Port, @@ -74,7 +86,7 @@ func main() { }).Info("enabling blackholer") b = &pancheri.Blackholer{ - DenyDomains: *new([]string), + DenyDomains: []string{}, } for _, file := range c.Blackhole.BlockLists { @@ -104,6 +116,10 @@ func main() { logrus.WithFields(logrus.Fields{ "file": file, }).Infof("loaded %d hosts", len(cfg.DenyDomains)) + + if !b.ShouldBlock("eu1.clevertap-prod.com") { + logrus.Errorf("failed sanity check!") + } } logrus.WithFields(logrus.Fields{ @@ -117,6 +133,7 @@ func main() { A: &pancheri.Authority{ Zones: zones, }, + B: b, } server := &dns.Server{ Addr: c.Server.Host + ":" + c.Server.Port, diff --git a/go.mod b/go.mod index a16670e..85969ad 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,20 @@ module git.e3t.cc/e3team/pancheri go 1.21 require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/miekg/dns v1.1.56 // indirect + github.com/prometheus/client_golang v1.17.0 // indirect + github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect + github.com/prometheus/common v0.44.0 // indirect + github.com/prometheus/procfs v0.11.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect golang.org/x/mod v0.12.0 // indirect golang.org/x/net v0.15.0 // indirect golang.org/x/sys v0.12.0 // indirect golang.org/x/tools v0.13.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index d8ef838..11ea002 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,27 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/miekg/dns v1.1.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE= github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= +github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= +github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM= +github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= +github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= +github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI= +github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -11,11 +30,17 @@ golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/handler.go b/handler.go index 021ccba..344ec6b 100644 --- a/handler.go +++ b/handler.go @@ -2,6 +2,8 @@ package pancheri import ( "github.com/miekg/dns" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" "github.com/sirupsen/logrus" "strings" ) @@ -13,14 +15,58 @@ type Handler struct { B *Blackholer } -func (h *Handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { +var ( + numberOfRequests = promauto.NewCounter(prometheus.CounterOpts{ + Name: "pancheri_handler_requests_total", + Help: "Number of DNS requests that have been handled by pancheri", + }) + numberOfARequests = promauto.NewCounter(prometheus.CounterOpts{ + Name: "pancheri_handler_a_requests_total", + Help: "Number of type A DNS requests that have been handled by pancheri", + }) + numberOfARequestsResultingInCNAME = promauto.NewCounter(prometheus.CounterOpts{ + Name: "pancheri_handler_a_to_cname_requests_total", + Help: "Number of type A DNS requests that resolved as a CNAME that have been handled by pancheri", + }) + numberOfAAAARequests = promauto.NewCounter(prometheus.CounterOpts{ + Name: "pancheri_handler_aaaa_requests_total", + Help: "Number of type A DNS requests that have been handled by pancheri", + }) + numberOfAAAARequestsResultingInCNAME = promauto.NewCounter(prometheus.CounterOpts{ + Name: "pancheri_handler_aaaa_to_cname_requests_total", + Help: "Number of type AAAA DNS requests that resolved as a CNAME that have been handled by pancheri", + }) + numberOfCNAMERequests = promauto.NewCounter(prometheus.CounterOpts{ + Name: "pancheri_handler_cname_requests_total", + Help: "Number of type CNAME DNS requests that have been handled by pancheri", + }) + numberOfTXTRequests = promauto.NewCounter(prometheus.CounterOpts{ + Name: "pancheri_handler_txt_requests_total", + Help: "Number of type CNAME DNS requests that have been handled by pancheri", + }) + numberOfUnsupportedRequests = promauto.NewCounter(prometheus.CounterOpts{ + Name: "pancheri_handler_unsupported_requests_total", + Help: "Number of unsupported type requests that have been handled by pancheri", + }) + numberOfMultiQuestionRequests = promauto.NewCounter(prometheus.CounterOpts{ + Name: "pancheri_handler_multiq_requests_total", + Help: "Number of unsupported multi-question requests that have been handled by pancheri", + }) + numberOfUpstreamedRequests = promauto.NewCounter(prometheus.CounterOpts{ + Name: "pancheri_handler_upstreamed_requests_total", + Help: "Number of requests that were sent upstream", + }) +) +func (h *Handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { + numberOfRequests.Inc() // figure out how we should resolve this msg := new(dns.Msg) msg.SetReply(r) msg.Authoritative = true if len(r.Question) != 1 { + numberOfMultiQuestionRequests.Inc() msg.Rcode = dns.RcodeFormatError err := w.WriteMsg(msg) if err != nil { @@ -32,6 +78,23 @@ func (h *Handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { q := r.Question[0] + // is it blackholed? + if h.B.ShouldBlock(strings.TrimRight(q.Name, ".")) { + // return nxdomain + // alright, send an nxdomain + if r.RecursionDesired { + msg.RecursionAvailable = true + } + msg.Rcode = dns.RcodeNameError + + err := w.WriteMsg(msg) + if err != nil { + logrus.Errorf("error responding: %s", err) + return + } + return + } + // is it ours? for authority, zone := range h.A.Zones { if strings.HasSuffix(q.Name, authority) { @@ -44,11 +107,13 @@ func (h *Handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { }).Trace("responding to query for authoritative zone") if q.Qtype == dns.TypeA { + numberOfARequests.Inc() record, ok := zone.ARecords[q.Name] if !ok { // SPECIAL CASE: for A and AAAA records, return with a CNAME if and only if that cname exists cname, cok := zone.CNAMERecords[q.Name] if cok { + numberOfARequestsResultingInCNAME.Inc() // return with the CNAME record instead and resolve the CNAME rendered := cname.Render() msg.Rcode = dns.RcodeSuccess @@ -125,10 +190,12 @@ func (h *Handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { return } else if q.Qtype == dns.TypeAAAA { record, ok := zone.AAAARecords[q.Name] + numberOfAAAARequests.Inc() if !ok { // SPECIAL CASE: for A and AAAA records, return with a CNAME if and only if that cname exists cname, cok := zone.CNAMERecords[q.Name] if cok { + numberOfAAAARequestsResultingInCNAME.Inc() // return with the CNAME record instead and resolve the CNAME rendered := cname.Render() msg.Rcode = dns.RcodeSuccess @@ -205,6 +272,7 @@ func (h *Handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { } return } else if q.Qtype == dns.TypeCNAME { + numberOfCNAMERequests.Inc() record, ok := zone.CNAMERecords[q.Name] if !ok { // send an nxdomain @@ -235,6 +303,7 @@ func (h *Handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { } return } else if q.Qtype == dns.TypeTXT { + numberOfTXTRequests.Inc() record, ok := zone.TXTRecords[q.Name] if !ok { // send an nxdomain @@ -265,6 +334,7 @@ func (h *Handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { } return } else { + numberOfUnsupportedRequests.Inc() // not supported logrus.WithFields(logrus.Fields{ "name": q.Name, @@ -288,6 +358,7 @@ func (h *Handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { } // no. do we have upstream resolution enabled? if h.C.Resolver.Enable { + numberOfUpstreamedRequests.Inc() // alright, resolve it with the resolver resp, err := h.R.Resolve(q.Name, q.Qtype) resp.SetReply(r) diff --git a/stats.go b/stats.go new file mode 100644 index 0000000..e549bde --- /dev/null +++ b/stats.go @@ -0,0 +1,12 @@ +package pancheri + +import ( + "github.com/prometheus/client_golang/prometheus/promhttp" + "net/http" +) + +func PromMain() error { + http.Handle("/metrics", promhttp.Handler()) + err := http.ListenAndServe(":2112", nil) + return err +}