From acc8842faddd535b099e3100ffd7636fd01a9cfc Mon Sep 17 00:00:00 2001 From: Frank Olbricht Date: Sun, 9 Jan 2022 07:44:53 -0700 Subject: [PATCH] Support naming blocklists to help with logging (#201) * Support naming blocklists to help with logging * Support naming of lists in response blocklists too * Add list name to client-blocklist as well --- blocklist.go | 9 +++++---- blocklist_test.go | 6 +++--- blocklistdb-domain.go | 35 +++++++++++++++++++++++++++-------- blocklistdb-domain_test.go | 4 ++-- blocklistdb-hosts.go | 27 ++++++++++++++++++++------- blocklistdb-hosts_test.go | 5 +++-- blocklistdb-multi.go | 8 ++++---- blocklistdb-regexp.go | 13 +++++++------ blocklistdb.go | 10 +++++++++- cidr-db.go | 14 +++++++++----- cidr-db_test.go | 2 +- client-blocklist.go | 4 ++-- cmd/routedns/config.go | 1 + cmd/routedns/main.go | 26 +++++++++++++++++--------- doc/configuration.md | 14 +++++++------- geoip-db.go | 17 +++++++++++------ ip-db-multi.go | 8 ++++---- response-blocklist-ip.go | 10 +++++----- 18 files changed, 137 insertions(+), 76 deletions(-) diff --git a/blocklist.go b/blocklist.go index 8fbdda6..6de57f0 100644 --- a/blocklist.go +++ b/blocklist.go @@ -8,6 +8,7 @@ import ( "time" "github.com/miekg/dns" + "github.com/sirupsen/logrus" ) // Blocklist is a resolver that returns NXDOMAIN or a spoofed IP for every query that @@ -92,8 +93,8 @@ func (r *Blocklist) Resolve(q *dns.Msg, ci ClientInfo) (*dns.Msg, error) { // Forward to upstream or the optional allowlist-resolver immediately if there's a match in the allowlist if allowlistDB != nil { - if _, _, rule, ok := allowlistDB.Match(question); ok { - log = log.WithField("rule", rule) + if _, _, match, ok := allowlistDB.Match(question); ok { + log = log.WithFields(logrus.Fields{"list": match.List, "rule": match.Rule}) r.metrics.allowed.Add(1) if r.AllowListResolver != nil { log.WithField("resolver", r.AllowListResolver.String()).Debug("matched allowlist, forwarding") @@ -104,14 +105,14 @@ func (r *Blocklist) Resolve(q *dns.Msg, ci ClientInfo) (*dns.Msg, error) { } } - ip, name, rule, ok := blocklistDB.Match(question) + ip, name, match, ok := blocklistDB.Match(question) if !ok { // Didn't match anything, pass it on to the next resolver log.WithField("resolver", r.resolver.String()).Debug("forwarding unmodified query to resolver") r.metrics.allowed.Add(1) return r.resolver.Resolve(q, ci) } - log = log.WithField("rule", rule) + log = log.WithFields(logrus.Fields{"list": match.List, "rule": match.Rule}) r.metrics.blocked.Add(1) // If we got a name for the PTR query, respond to it diff --git a/blocklist_test.go b/blocklist_test.go index 280b0bd..26382ef 100644 --- a/blocklist_test.go +++ b/blocklist_test.go @@ -16,7 +16,7 @@ func TestBlocklistRegexp(t *testing.T) { `(^|\.)block\.test`, `(^|\.)evil\.test`, }) - m, err := NewRegexpDB(loader) + m, err := NewRegexpDB("testlist", loader) require.NoError(t, err) opt := BlocklistOptions{ @@ -51,9 +51,9 @@ func TestBlocklistAllow(t *testing.T) { allowloader := NewStaticLoader([]string{ `(^|\.)good\.evil\.test`, }) - blockDB, err := NewRegexpDB(blockloader) + blockDB, err := NewRegexpDB("testlist", blockloader) require.NoError(t, err) - allowDB, err := NewRegexpDB(allowloader) + allowDB, err := NewRegexpDB("testlist", allowloader) require.NoError(t, err) opt := BlocklistOptions{ diff --git a/blocklistdb-domain.go b/blocklistdb-domain.go index 2c08ae9..073f02a 100644 --- a/blocklistdb-domain.go +++ b/blocklistdb-domain.go @@ -14,6 +14,7 @@ import ( // .domain.com: matches domain.com and all subdomains // *.domain.com: matches all subdomains but not domain.com type DomainDB struct { + name string root node loader BlocklistLoader } @@ -23,7 +24,7 @@ type node map[string]node var _ BlocklistDB = &DomainDB{} // NewDomainDB returns a new instance of a matcher for a list of regular expressions. -func NewDomainDB(loader BlocklistLoader) (*DomainDB, error) { +func NewDomainDB(name string, loader BlocklistLoader) (*DomainDB, error) { rules, err := loader.Load() if err != nil { return nil, err @@ -55,14 +56,14 @@ func NewDomainDB(loader BlocklistLoader) (*DomainDB, error) { n = subNode } } - return &DomainDB{root, loader}, nil + return &DomainDB{name, root, loader}, nil } func (m *DomainDB) Reload() (BlocklistDB, error) { - return NewDomainDB(m.loader) + return NewDomainDB(m.name, m.loader) } -func (m *DomainDB) Match(q dns.Question) (net.IP, string, string, bool) { +func (m *DomainDB) Match(q dns.Question) (net.IP, string, *BlocklistMatch, bool) { s := strings.TrimSuffix(q.Name, ".") var matched []string parts := strings.Split(s, ".") @@ -71,18 +72,36 @@ func (m *DomainDB) Match(q dns.Question) (net.IP, string, string, bool) { part := parts[i] subNode, ok := n[part] if !ok { - return nil, "", "", false + return nil, "", nil, false } matched = append(matched, part) if _, ok := subNode[""]; ok { // exact and sub-domain match - return nil, "", matchedDomainParts(".", matched), true + return nil, + "", + &BlocklistMatch{ + List: m.name, + Rule: matchedDomainParts(".", matched), + }, + true } if _, ok := subNode["*"]; ok && i > 0 { // wildcard match on sub-domains - return nil, "", matchedDomainParts("*.", matched), true + return nil, + "", + &BlocklistMatch{ + List: m.name, + Rule: matchedDomainParts("*.", matched), + }, + true } n = subNode } - return nil, "", matchedDomainParts("", matched), len(n) == 0 // exact match + return nil, + "", + &BlocklistMatch{ + List: m.name, + Rule: matchedDomainParts("", matched), + }, + len(n) == 0 // exact match } func (m *DomainDB) String() string { diff --git a/blocklistdb-domain_test.go b/blocklistdb-domain_test.go index 8ba7d3d..e102b7f 100644 --- a/blocklistdb-domain_test.go +++ b/blocklistdb-domain_test.go @@ -18,7 +18,7 @@ func TestDomainDB(t *testing.T) { ".domain4.com", }) - m, err := NewDomainDB(loader) + m, err := NewDomainDB("testlist", loader) require.NoError(t, err) tests := []struct { @@ -61,7 +61,7 @@ func TestDomainDBError(t *testing.T) { } for _, test := range tests { loader := NewStaticLoader([]string{test.name}) - _, err := NewDomainDB(loader) + _, err := NewDomainDB("testlist", loader) require.Error(t, err) } } diff --git a/blocklistdb-hosts.go b/blocklistdb-hosts.go index 0f3bdbd..a585344 100644 --- a/blocklistdb-hosts.go +++ b/blocklistdb-hosts.go @@ -11,6 +11,7 @@ import ( // IP4 and IP6 records can be spoofed independently, however it's not possible to block only one type. If // IP4 is given but no IP6, then a domain match will still result in an NXDOMAIN for the IP6 address. type HostsDB struct { + name string filters map[string]ipRecords ptrMap map[string]string // PTR lookup map loader BlocklistLoader @@ -24,7 +25,7 @@ type ipRecords struct { var _ BlocklistDB = &HostsDB{} // NewHostsDB returns a new instance of a matcher for a list of regular expressions. -func NewHostsDB(loader BlocklistLoader) (*HostsDB, error) { +func NewHostsDB(name string, loader BlocklistLoader) (*HostsDB, error) { rules, err := loader.Load() if err != nil { return nil, err @@ -69,24 +70,36 @@ func NewHostsDB(loader BlocklistLoader) (*HostsDB, error) { } ptrMap[reverseAddr] = names[0] } - return &HostsDB{filters, ptrMap, loader}, nil + return &HostsDB{name, filters, ptrMap, loader}, nil } func (m *HostsDB) Reload() (BlocklistDB, error) { - return NewHostsDB(m.loader) + return NewHostsDB(m.name, m.loader) } -func (m *HostsDB) Match(q dns.Question) (net.IP, string, string, bool) { +func (m *HostsDB) Match(q dns.Question) (net.IP, string, *BlocklistMatch, bool) { if q.Qtype == dns.TypePTR { name, ok := m.ptrMap[q.Name] - return nil, name, "", ok + return nil, name, nil, ok } name := strings.TrimSuffix(q.Name, ".") ips, ok := m.filters[name] if q.Qtype == dns.TypeA { - return ips.ip4, "", ips.ip4.String() + " " + name, ok + return ips.ip4, + "", + &BlocklistMatch{ + List: m.name, + Rule: ips.ip4.String() + " " + name, + }, + ok } - return ips.ip6, "", ips.ip6.String() + " " + name, ok + return ips.ip6, + "", + &BlocklistMatch{ + List: m.name, + Rule: ips.ip6.String() + " " + name, + }, + ok } func (m *HostsDB) String() string { diff --git a/blocklistdb-hosts_test.go b/blocklistdb-hosts_test.go index 63bbcf0..1159a55 100644 --- a/blocklistdb-hosts_test.go +++ b/blocklistdb-hosts_test.go @@ -20,7 +20,7 @@ func TestHostsDB(t *testing.T) { "192.168.1.1 domain6.com", }) - m, err := NewHostsDB(loader) + m, err := NewHostsDB("testlist", loader) require.NoError(t, err) tests := []struct { @@ -39,8 +39,9 @@ func TestHostsDB(t *testing.T) { } for _, test := range tests { q := dns.Question{Name: test.q, Qtype: test.typ, Qclass: dns.ClassINET} - ip, _, _, ok := m.Match(q) + ip, _, match, ok := m.Match(q) require.Equal(t, test.match, ok, "query: %s", test.q) require.Equal(t, test.ip, ip, "query: %s", test.q) + require.Equal(t, "testlist", match.List) } } diff --git a/blocklistdb-multi.go b/blocklistdb-multi.go index 3fdf453..9cfca83 100644 --- a/blocklistdb-multi.go +++ b/blocklistdb-multi.go @@ -30,13 +30,13 @@ func (m MultiDB) Reload() (BlocklistDB, error) { return NewMultiDB(newDBs...) } -func (m MultiDB) Match(q dns.Question) (net.IP, string, string, bool) { +func (m MultiDB) Match(q dns.Question) (net.IP, string, *BlocklistMatch, bool) { for _, db := range m.dbs { - if ip, name, rule, ok := db.Match(q); ok { - return ip, name, rule, ok + if ip, name, match, ok := db.Match(q); ok { + return ip, name, match, ok } } - return nil, "", "", false + return nil, "", nil, false } func (m MultiDB) String() string { diff --git a/blocklistdb-regexp.go b/blocklistdb-regexp.go index f5f6a58..f119590 100644 --- a/blocklistdb-regexp.go +++ b/blocklistdb-regexp.go @@ -10,6 +10,7 @@ import ( // RegexpDB holds a list of regular expressions against which it evaluates DNS queries. type RegexpDB struct { + name string rules []*regexp.Regexp loader BlocklistLoader } @@ -17,7 +18,7 @@ type RegexpDB struct { var _ BlocklistDB = &RegexpDB{} // NewRegexpDB returns a new instance of a matcher for a list of regular expressions. -func NewRegexpDB(loader BlocklistLoader) (*RegexpDB, error) { +func NewRegexpDB(name string, loader BlocklistLoader) (*RegexpDB, error) { rules, err := loader.Load() if err != nil { return nil, err @@ -35,20 +36,20 @@ func NewRegexpDB(loader BlocklistLoader) (*RegexpDB, error) { filters = append(filters, re) } - return &RegexpDB{filters, loader}, nil + return &RegexpDB{name, filters, loader}, nil } func (m *RegexpDB) Reload() (BlocklistDB, error) { - return NewRegexpDB(m.loader) + return NewRegexpDB(m.name, m.loader) } -func (m *RegexpDB) Match(q dns.Question) (net.IP, string, string, bool) { +func (m *RegexpDB) Match(q dns.Question) (net.IP, string, *BlocklistMatch, bool) { for _, rule := range m.rules { if rule.MatchString(q.Name) { - return nil, "", rule.String(), true + return nil, "", &BlocklistMatch{List: m.name, Rule: rule.String()}, true } } - return nil, "", "", false + return nil, "", nil, false } func (m *RegexpDB) String() string { diff --git a/blocklistdb.go b/blocklistdb.go index 336bd62..ec0dd3d 100644 --- a/blocklistdb.go +++ b/blocklistdb.go @@ -14,7 +14,15 @@ type BlocklistDB interface { // Returns true if the question matches a rule. If the IP is not nil, // respond with the given IP. NXDOMAIN otherwise. - Match(q dns.Question) (net.IP, string, string, bool) + Match(q dns.Question) (net.IP, string, *BlocklistMatch, bool) fmt.Stringer } + +// BlocklistMatch is returned by blocklists when a match is found. It contains +// information about what rule matched, what list it was from etc. Used mostly +// for logging. +type BlocklistMatch struct { + List string // Identifier or name of the blocklist + Rule string // Identifier for the rule that matched +} diff --git a/cidr-db.go b/cidr-db.go index 63e7eb5..b63a973 100644 --- a/cidr-db.go +++ b/cidr-db.go @@ -9,6 +9,7 @@ import ( // Network ranges are stored in a trie (one for IP4 and one for IP6) to allow for // efficient matching type CidrDB struct { + name string ip4, ip6 *ipBlocklistTrie loader BlocklistLoader } @@ -16,12 +17,13 @@ type CidrDB struct { var _ IPBlocklistDB = &CidrDB{} // NewCidrDB returns a new instance of a matcher for a list of networks. -func NewCidrDB(loader BlocklistLoader) (*CidrDB, error) { +func NewCidrDB(name string, loader BlocklistLoader) (*CidrDB, error) { rules, err := loader.Load() if err != nil { return nil, err } db := &CidrDB{ + name: name, ip4: new(ipBlocklistTrie), ip6: new(ipBlocklistTrie), loader: loader, @@ -53,14 +55,16 @@ func NewCidrDB(loader BlocklistLoader) (*CidrDB, error) { } func (m *CidrDB) Reload() (IPBlocklistDB, error) { - return NewCidrDB(m.loader) + return NewCidrDB(m.name, m.loader) } -func (m *CidrDB) Match(ip net.IP) (string, bool) { +func (m *CidrDB) Match(ip net.IP) (*BlocklistMatch, bool) { if addr := ip.To4(); addr == nil { - return m.ip6.hasIP(ip) + rule, ok := m.ip6.hasIP(ip) + return &BlocklistMatch{List: m.name, Rule: rule}, ok } - return m.ip4.hasIP(ip) + rule, ok := m.ip4.hasIP(ip) + return &BlocklistMatch{List: m.name, Rule: rule}, ok } func (m *CidrDB) Close() error { diff --git a/cidr-db_test.go b/cidr-db_test.go index 00328ce..134f3ce 100644 --- a/cidr-db_test.go +++ b/cidr-db_test.go @@ -13,7 +13,7 @@ func TestCidrDB(t *testing.T) { "1.2.0.0/16", "2a03:2880:f101:83::0/64", }) - db, err := NewCidrDB(loader) + db, err := NewCidrDB("testlist", loader) require.NoError(t, err) tests := []struct { diff --git a/client-blocklist.go b/client-blocklist.go index c3df259..88df348 100644 --- a/client-blocklist.go +++ b/client-blocklist.go @@ -49,8 +49,8 @@ func NewClientBlocklist(id string, resolver Resolver, opt ClientBlocklistOptions // REFUSED if the client IP is on the blocklist, or sends the query to an alternative // resolver if one is configured. func (r *ClientBlocklist) Resolve(q *dns.Msg, ci ClientInfo) (*dns.Msg, error) { - if rule, ok := r.BlocklistDB.Match(ci.SourceIP); ok { - log := Log.WithFields(logrus.Fields{"id": r.id, "qname": qName(q), "rule": rule, "ip": ci.SourceIP}) + if match, ok := r.BlocklistDB.Match(ci.SourceIP); ok { + log := Log.WithFields(logrus.Fields{"id": r.id, "qname": qName(q), "list": match.List, "rule": match.Rule, "ip": ci.SourceIP}) r.metrics.blocked.Add(1) if r.BlocklistResolver != nil { log.WithField("resolver", r.BlocklistResolver).Debug("client on blocklist, forwarding to blocklist-resolver") diff --git a/cmd/routedns/config.go b/cmd/routedns/config.go index 834b34b..e084d62 100644 --- a/cmd/routedns/config.go +++ b/cmd/routedns/config.go @@ -127,6 +127,7 @@ type group struct { // Block/Allowlist items for blocklist-v2 type list struct { + Name string Format string Source string CacheDir string `toml:"cache-dir"` // Where to store copies of remote blocklists for faster startup diff --git a/cmd/routedns/main.go b/cmd/routedns/main.go index 023ad38..d2a66ec 100644 --- a/cmd/routedns/main.go +++ b/cmd/routedns/main.go @@ -323,7 +323,7 @@ func instantiateGroup(id string, g group, resolvers map[string]rdns.Resolver) er if len(g.Blocklist) > 0 && g.Source != "" { return fmt.Errorf("static blocklist can't be used with 'source' in '%s'", id) } - blocklistDB, err := newBlocklistDB(list{Format: g.Format, Source: g.Source}, g.Blocklist) + blocklistDB, err := newBlocklistDB(list{Name: id, Format: g.Format, Source: g.Source}, g.Blocklist) if err != nil { return err } @@ -347,7 +347,7 @@ func instantiateGroup(id string, g group, resolvers map[string]rdns.Resolver) er } var blocklistDB rdns.BlocklistDB if len(g.Blocklist) > 0 { - blocklistDB, err = newBlocklistDB(list{Format: g.BlocklistFormat}, g.Blocklist) + blocklistDB, err = newBlocklistDB(list{Name: id, Format: g.BlocklistFormat}, g.Blocklist) if err != nil { return err } @@ -506,7 +506,7 @@ func instantiateGroup(id string, g group, resolvers map[string]rdns.Resolver) er } var blocklistDB rdns.IPBlocklistDB if len(g.Blocklist) > 0 { - blocklistDB, err = newIPBlocklistDB(list{Format: g.BlocklistFormat}, g.LocationDB, g.Blocklist) + blocklistDB, err = newIPBlocklistDB(list{Name: id, Format: g.BlocklistFormat}, g.LocationDB, g.Blocklist) if err != nil { return err } @@ -579,7 +579,7 @@ func instantiateGroup(id string, g group, resolvers map[string]rdns.Resolver) er } var blocklistDB rdns.IPBlocklistDB if len(g.Blocklist) > 0 { - blocklistDB, err = newIPBlocklistDB(list{Format: g.BlocklistFormat}, g.LocationDB, g.Blocklist) + blocklistDB, err = newIPBlocklistDB(list{Name: id, Format: g.BlocklistFormat}, g.LocationDB, g.Blocklist) if err != nil { return err } @@ -680,6 +680,10 @@ func newBlocklistDB(l list, rules []string) (rdns.BlocklistDB, error) { if err != nil { return nil, err } + name := l.Name + if name == "" { + name = l.Source + } var loader rdns.BlocklistLoader if len(rules) > 0 { loader = rdns.NewStaticLoader(rules) @@ -698,11 +702,11 @@ func newBlocklistDB(l list, rules []string) (rdns.BlocklistDB, error) { } switch l.Format { case "regexp", "": - return rdns.NewRegexpDB(loader) + return rdns.NewRegexpDB(name, loader) case "domain": - return rdns.NewDomainDB(loader) + return rdns.NewDomainDB(name, loader) case "hosts": - return rdns.NewHostsDB(loader) + return rdns.NewHostsDB(name, loader) default: return nil, fmt.Errorf("unsupported format '%s'", l.Format) } @@ -713,6 +717,10 @@ func newIPBlocklistDB(l list, locationDB string, rules []string) (rdns.IPBlockli if err != nil { return nil, err } + name := l.Name + if name == "" { + name = l.Source + } var loader rdns.BlocklistLoader if len(rules) > 0 { loader = rdns.NewStaticLoader(rules) @@ -732,9 +740,9 @@ func newIPBlocklistDB(l list, locationDB string, rules []string) (rdns.IPBlockli switch l.Format { case "cidr", "": - return rdns.NewCidrDB(loader) + return rdns.NewCidrDB(name, loader) case "location": - return rdns.NewGeoIPDB(loader, locationDB) + return rdns.NewGeoIPDB(name, loader, locationDB) default: return nil, fmt.Errorf("unsupported format '%s'", l.Format) } diff --git a/doc/configuration.md b/doc/configuration.md index 6b247c8..3e0ec18 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -531,7 +531,7 @@ Options: - `blocklist-resolver` - Alternative resolver for queries matching the blocklist, rather than responding with NXDOMAIN. Optional. - `blocklist-format` - The format the blocklist is provided in. Only used if `blocklist-source` is not provided. Can be `regexp`, `domain`, or `hosts`. Defaults to `regexp`. - `blocklist-refresh` - Time interval (in seconds) in which external (remote or local) blocklists are reloaded. Optional. -- `blocklist-source` - An array of blocklists, each with `format` and `source`. +- `blocklist-source` - An array of blocklists, each with `format`, `source` and optionally `name`. - `allowlist-resolver` - Alternative resolver for queries matching the allowlist, rather than forwarding to the default resolver. - `allowlist-format` - The format the allowlist is provided in. Only used if `allowlist-source` is not provided. Can be `regexp`, `domain`, or `hosts`. Defaults to `regexp`. - `allowlist-refresh` - Time interval (in seconds) in which external allowlists are reloaded. Optional. @@ -580,7 +580,7 @@ blocklist = [ ] ``` -Blocklist that loads two rule-sets. One from an HTTP server, the other from a file on disk. Both are reloaded once a day. +Blocklist that loads two rule-sets. One from an HTTP server, the other from a file on disk. Both are reloaded once a day. A `name` can be provided which will be used in logs instead of `source`. ```toml [groups.cloudflare-blocklist] @@ -588,7 +588,7 @@ type = "blocklist-v2" resolvers = ["cloudflare-dot"] blocklist-refresh = 86400 blocklist-source = [ - {format = "domain", source = "https://raw.githubusercontent.com/cbuijs/accomplist/master/deugniets/routedns.blocklist.domain.list"}, + {name = "cbuijs/blocklist" format = "domain", source = "https://raw.githubusercontent.com/cbuijs/accomplist/master/deugniets/routedns.blocklist.domain.list"}, {format = "regexp", source = "/path/to/local/regexp.list"}, ] ``` @@ -646,7 +646,7 @@ Options: - For `response-blocklist-ip`, the value can be `cidr`, or `location`. Defaults to `cidr`. - For `response-blocklist-name`, the value can be `regexp`, `domain`, or `hosts`. Defaults to `regexp`. - `blocklist-refresh` - Time interval (in seconds) in which external (remote or local) blocklists are reloaded. Optional. -- `blocklist-source` - An array of blocklists, each with `format`, `source` and optionally `cache-dir` (see notes for [Query Blockists](#Query-Blocklist)). +- `blocklist-source` - An array of blocklists, each with `format`, `source` and optionally `cache-dir` (see notes for [Query Blockists](#Query-Blocklist)) as well as `name` which assigns a name to the list used in logs (defaults to `source`). - `filter` - If set to `true` in `response-blocklist-ip`, matching records will be removed from responses rather than the whole response. If there is no answer record left after applying the filter, NXDOMAIN will be returned unless an alternative `blocklist-resolver` is defined. - `location-db` - If location-based IP blocking is used, this specifies the GeoIP data file to load. Optional. Defaults to /usr/share/GeoIP/GeoLite2-City.mmdb @@ -700,7 +700,7 @@ blocklist-source = [ ] ``` -Response blocklist that is cached on local disk for faster startup +Response blocklist that is cached on local disk for faster startup. By default, logs will contain the source (in this case the URL) of a match, but different name can be specified with `name`. ```toml [groups.cloudflare-blocklist] @@ -708,7 +708,7 @@ type = "response-blocklist-ip" resolvers = ["cloudflare-dot"] blocklist-refresh = 86400 blocklist-source = [ - {source = "https://host/block.cidr.txt", cache-dir="/var/tmp"}, + {name = "my-block-list", source = "https://host/block.cidr.txt", cache-dir="/var/tmp"}, ] ``` @@ -744,7 +744,7 @@ Options: - `blocklist-resolver` - Alternative resolver for responses matching a rule, the query will be re-sent to this resolver. Optional. - `blocklist-format` - The format the blocklist is provided in. Only used if `blocklist-source` is not provided. Values can be `cidr`, or `location`. Defaults to `cidr`. - `blocklist-refresh` - Time interval (in seconds) in which external (remote or local) blocklists are reloaded. Optional. -- `blocklist-source` - An array of blocklists, each with `format` and `source`. +- `blocklist-source` - An array of blocklists, each with `format` and `source` and optionally `name`. - `location-db` - If location-based IP blocking is used, this specifies the GeoIP data file to load. Optional. Defaults to /usr/share/GeoIP/GeoLite2-City.mmdb Examples: diff --git a/geoip-db.go b/geoip-db.go index bad6a5c..bc7605a 100644 --- a/geoip-db.go +++ b/geoip-db.go @@ -13,6 +13,7 @@ import ( // its location is looked up in a database and the result is compared to the // blocklist rules. type GeoIPDB struct { + name string loader BlocklistLoader geoDB *maxminddb.Reader geoDBFile string @@ -22,7 +23,7 @@ type GeoIPDB struct { var _ IPBlocklistDB = &GeoIPDB{} // NewGeoIPDB returns a new instance of a matcher for a location rules. -func NewGeoIPDB(loader BlocklistLoader, geoDBFile string) (*GeoIPDB, error) { +func NewGeoIPDB(name string, loader BlocklistLoader, geoDBFile string) (*GeoIPDB, error) { if geoDBFile == "" { geoDBFile = "/usr/share/GeoIP/GeoLite2-City.mmdb" } @@ -51,6 +52,7 @@ func NewGeoIPDB(loader BlocklistLoader, geoDBFile string) (*GeoIPDB, error) { db[value] = struct{}{} } return &GeoIPDB{ + name: name, geoDB: geoDB, geoDBFile: geoDBFile, db: db, @@ -59,10 +61,10 @@ func NewGeoIPDB(loader BlocklistLoader, geoDBFile string) (*GeoIPDB, error) { } func (m *GeoIPDB) Reload() (IPBlocklistDB, error) { - return NewGeoIPDB(m.loader, m.geoDBFile) + return NewGeoIPDB(m.name, m.loader, m.geoDBFile) } -func (m *GeoIPDB) Match(ip net.IP) (string, bool) { +func (m *GeoIPDB) Match(ip net.IP) (*BlocklistMatch, bool) { var record struct { Continent struct { GeoNameID uint64 `maxminddb:"geoname_id"` @@ -80,7 +82,7 @@ func (m *GeoIPDB) Match(ip net.IP) (string, bool) { if err := m.geoDB.Lookup(ip, &record); err != nil { Log.WithField("ip", ip).WithError(err).Error("failed to lookup ip in geo location database") - return "", false + return nil, false } // Try to find the continent, country, or city GeoName ID in the blocklist @@ -90,10 +92,13 @@ func (m *GeoIPDB) Match(ip net.IP) (string, bool) { } for _, id := range ids { if _, ok := m.db[id]; ok { - return fmt.Sprintf("%d", id), true + return &BlocklistMatch{ + List: m.name, + Rule: fmt.Sprintf("%d", id), + }, true } } - return "", false + return nil, false } func (m *GeoIPDB) Close() error { diff --git a/ip-db-multi.go b/ip-db-multi.go index d6de77f..35284ff 100644 --- a/ip-db-multi.go +++ b/ip-db-multi.go @@ -28,13 +28,13 @@ func (m MultiIPDB) Reload() (IPBlocklistDB, error) { return NewMultiIPDB(newDBs...) } -func (m MultiIPDB) Match(ip net.IP) (string, bool) { +func (m MultiIPDB) Match(ip net.IP) (*BlocklistMatch, bool) { for _, db := range m.dbs { - if rule, ok := db.Match(ip); ok { - return rule, ok + if match, ok := db.Match(ip); ok { + return match, ok } } - return "", false + return nil, false } func (m MultiIPDB) Close() error { diff --git a/response-blocklist-ip.go b/response-blocklist-ip.go index f623892..d06b0ea 100644 --- a/response-blocklist-ip.go +++ b/response-blocklist-ip.go @@ -13,7 +13,7 @@ import ( // IPBlocklistDB is a database containing IPs used in blocklists. type IPBlocklistDB interface { Reload() (IPBlocklistDB, error) - Match(ip net.IP) (string, bool) + Match(ip net.IP) (*BlocklistMatch, bool) Close() error fmt.Stringer } @@ -103,8 +103,8 @@ func (r *ResponseBlocklistIP) blockIfMatch(query, answer *dns.Msg, ci ClientInfo default: continue } - if rule, ok := r.BlocklistDB.Match(ip); ok { - log := logger(r.id, query, ci).WithFields(logrus.Fields{"rule": rule, "ip": ip}) + if match, ok := r.BlocklistDB.Match(ip); ok { + log := logger(r.id, query, ci).WithFields(logrus.Fields{"list": match.List, "rule": match.Rule, "ip": ip}) if r.BlocklistResolver != nil { log.WithField("resolver", r.BlocklistResolver).Debug("blocklist match, forwarding to blocklist-resolver") return r.BlocklistResolver.Resolve(query, ci) @@ -147,8 +147,8 @@ func (r *ResponseBlocklistIP) filterRR(query *dns.Msg, ci ClientInfo, rrs []dns. newRRs = append(newRRs, rr) continue } - if rule, ok := r.BlocklistDB.Match(ip); ok { - logger(r.id, query, ci).WithFields(logrus.Fields{"rule": rule, "ip": ip}).Debug("filtering response") + if match, ok := r.BlocklistDB.Match(ip); ok { + logger(r.id, query, ci).WithFields(logrus.Fields{"list": match.List, "rule": match.Rule, "ip": ip}).Debug("filtering response") continue } newRRs = append(newRRs, rr)