From 65e6b8569ee65e168095d9cfb562681491df22f0 Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Sat, 7 Apr 2018 23:02:40 +0200 Subject: [PATCH] Implement whitelists Fixes #293 --- dnscrypt-proxy/config.go | 19 +++ dnscrypt-proxy/example-dnscrypt-proxy.toml | 28 +++++ dnscrypt-proxy/example-whitelist.txt | 22 ++++ dnscrypt-proxy/plugin_block_ip.go | 3 + dnscrypt-proxy/plugin_block_name.go | 3 + dnscrypt-proxy/plugin_whitelist_name.go | 132 +++++++++++++++++++++ dnscrypt-proxy/plugins.go | 3 + dnscrypt-proxy/proxy.go | 3 + 8 files changed, 213 insertions(+) create mode 100644 dnscrypt-proxy/example-whitelist.txt create mode 100644 dnscrypt-proxy/plugin_whitelist_name.go diff --git a/dnscrypt-proxy/config.go b/dnscrypt-proxy/config.go index eadae3ce..e588f01c 100644 --- a/dnscrypt-proxy/config.go +++ b/dnscrypt-proxy/config.go @@ -37,6 +37,7 @@ type Config struct { QueryLog QueryLogConfig `toml:"query_log"` NxLog NxLogConfig `toml:"nx_log"` BlockName BlockNameConfig `toml:"blacklist"` + WhitelistName WhitelistNameConfig `toml:"whitelist"` BlockIP BlockIPConfig `toml:"ip_blacklist"` ForwardFile string `toml:"forwarding_rules"` CloakFile string `toml:"cloaking_rules"` @@ -121,6 +122,12 @@ type BlockNameConfig struct { Format string `toml:"log_format"` } +type WhitelistNameConfig struct { + File string `toml:"whitelist_file"` + LogFile string `toml:"log_file"` + Format string `toml:"log_format"` +} + type BlockIPConfig struct { File string `toml:"blacklist_file"` LogFile string `toml:"log_file"` @@ -294,6 +301,18 @@ func ConfigLoad(proxy *Proxy, svcFlag *string) error { proxy.blockNameFormat = config.BlockName.Format proxy.blockNameLogFile = config.BlockName.LogFile + if len(config.WhitelistName.Format) == 0 { + config.WhitelistName.Format = "tsv" + } else { + config.WhitelistName.Format = strings.ToLower(config.WhitelistName.Format) + } + if config.WhitelistName.Format != "tsv" && config.WhitelistName.Format != "ltsv" { + return errors.New("Unsupported whitelist log format") + } + proxy.whitelistNameFile = config.WhitelistName.File + proxy.whitelistNameFormat = config.WhitelistName.Format + proxy.whitelistNameLogFile = config.WhitelistName.LogFile + if len(config.BlockIP.Format) == 0 { config.BlockIP.Format = "tsv" } else { diff --git a/dnscrypt-proxy/example-dnscrypt-proxy.toml b/dnscrypt-proxy/example-dnscrypt-proxy.toml index dd8f2d22..84969104 100644 --- a/dnscrypt-proxy/example-dnscrypt-proxy.toml +++ b/dnscrypt-proxy/example-dnscrypt-proxy.toml @@ -341,6 +341,34 @@ cache_neg_ttl = 60 +###################################################### +# Pattern-based whitelisting (blacklists bypass) # +###################################################### + +## Whitelists support the same patterns as blacklists +## If a name matches a whitelist entry, the corresponding session +## will bypass names and IP filters. +## +## Time-based rules are also supported to make some websites only accessible at specific times of the day. + +[whitelist] + + ## Path to the file of whitelisting rules (absolute, or relative to the same directory as the executable file) + + # whitelist_file = 'whitelist.txt' + + + ## Optional path to a file logging whitelisted queries + + # log_file = 'whitelisted.log' + + + ## Optional log format: tsv or ltsv (default: tsv) + + # log_format = 'tsv' + + + ########################################## # Time access restrictions # ########################################## diff --git a/dnscrypt-proxy/example-whitelist.txt b/dnscrypt-proxy/example-whitelist.txt new file mode 100644 index 00000000..c5accd10 --- /dev/null +++ b/dnscrypt-proxy/example-whitelist.txt @@ -0,0 +1,22 @@ + +########################### +# Whitelist # +########################### + +## Rules for name-based query whitelisting, one per line +## +## Example of valid patterns: +## +## ads.* | matches anything with an "ads." prefix +## *.example.com | matches example.com and all names within that zone such as www.example.com +## example.com | identical to the above +## *sex* | matches any name containing that substring +## ads[0-9]* | matches "ads" followed by one or more digits +## ads*.example* | *, ? and [] can be used anywhere, but prefixes/suffixes are faster + + + +## Time-based rules + +# *.youtube.* @time-to-play +# facebook.com @play diff --git a/dnscrypt-proxy/plugin_block_ip.go b/dnscrypt-proxy/plugin_block_ip.go index 5f868ad2..29abf308 100644 --- a/dnscrypt-proxy/plugin_block_ip.go +++ b/dnscrypt-proxy/plugin_block_ip.go @@ -88,6 +88,9 @@ func (plugin *PluginBlockIP) Reload() error { } func (plugin *PluginBlockIP) Eval(pluginsState *PluginsState, msg *dns.Msg) error { + if pluginsState.sessionData["whitelisted"] != nil { + return nil + } answers := msg.Answer if len(answers) == 0 { return nil diff --git a/dnscrypt-proxy/plugin_block_name.go b/dnscrypt-proxy/plugin_block_name.go index 03418136..5b207bc7 100644 --- a/dnscrypt-proxy/plugin_block_name.go +++ b/dnscrypt-proxy/plugin_block_name.go @@ -83,6 +83,9 @@ func (plugin *PluginBlockName) Reload() error { } func (plugin *PluginBlockName) Eval(pluginsState *PluginsState, msg *dns.Msg) error { + if pluginsState.sessionData["whitelisted"] != nil { + return nil + } questions := msg.Question if len(questions) != 1 { return nil diff --git a/dnscrypt-proxy/plugin_whitelist_name.go b/dnscrypt-proxy/plugin_whitelist_name.go new file mode 100644 index 00000000..1e235970 --- /dev/null +++ b/dnscrypt-proxy/plugin_whitelist_name.go @@ -0,0 +1,132 @@ +package main + +import ( + "errors" + "fmt" + "io/ioutil" + "net" + "strings" + "time" + "unicode" + + "github.com/jedisct1/dlog" + "github.com/miekg/dns" + lumberjack "gopkg.in/natefinch/lumberjack.v2" +) + +type PluginWhitelistName struct { + allWeeklyRanges *map[string]WeeklyRanges + patternMatcher *PatternMatcher + logger *lumberjack.Logger + format string +} + +func (plugin *PluginWhitelistName) Name() string { + return "whitelist_name" +} + +func (plugin *PluginWhitelistName) Description() string { + return "Whitelists DNS queries matching name patterns" +} + +func (plugin *PluginWhitelistName) Init(proxy *Proxy) error { + dlog.Noticef("Loading the set of whitelisting rules from [%s]", proxy.whitelistNameFile) + bin, err := ioutil.ReadFile(proxy.whitelistNameFile) + if err != nil { + return err + } + plugin.allWeeklyRanges = proxy.allWeeklyRanges + plugin.patternMatcher = NewPatternPatcher() + for lineNo, line := range strings.Split(string(bin), "\n") { + line = strings.TrimFunc(line, unicode.IsSpace) + if len(line) == 0 || strings.HasPrefix(line, "#") { + continue + } + parts := strings.Split(line, "@") + timeRangeName := "" + if len(parts) == 2 { + line = strings.TrimFunc(parts[0], unicode.IsSpace) + timeRangeName = strings.TrimFunc(parts[1], unicode.IsSpace) + } else if len(parts) > 2 { + dlog.Errorf("Syntax error in whitelist rules at line %d -- Unexpected @ character", 1+lineNo) + continue + } + var weeklyRanges *WeeklyRanges + if len(timeRangeName) > 0 { + weeklyRangesX, ok := (*plugin.allWeeklyRanges)[timeRangeName] + if !ok { + dlog.Errorf("Time range [%s] not found at line %d", timeRangeName, 1+lineNo) + } else { + weeklyRanges = &weeklyRangesX + } + } + if _, err := plugin.patternMatcher.Add(line, weeklyRanges, lineNo+1); err != nil { + dlog.Error(err) + continue + } + } + if len(proxy.whitelistNameLogFile) == 0 { + return nil + } + plugin.logger = &lumberjack.Logger{LocalTime: true, MaxSize: proxy.logMaxSize, MaxAge: proxy.logMaxAge, MaxBackups: proxy.logMaxBackups, Filename: proxy.whitelistNameLogFile, Compress: true} + plugin.format = proxy.whitelistNameFormat + + return nil +} + +func (plugin *PluginWhitelistName) Drop() error { + return nil +} + +func (plugin *PluginWhitelistName) Reload() error { + return nil +} + +func (plugin *PluginWhitelistName) Eval(pluginsState *PluginsState, msg *dns.Msg) error { + questions := msg.Question + if len(questions) != 1 { + return nil + } + qName := strings.ToLower(StripTrailingDot(questions[0].Name)) + whitelist, reason, xweeklyRanges := plugin.patternMatcher.Eval(qName) + var weeklyRanges *WeeklyRanges + if xweeklyRanges != nil { + weeklyRanges = xweeklyRanges.(*WeeklyRanges) + } + if whitelist { + if weeklyRanges != nil && !weeklyRanges.Match() { + whitelist = false + } + } + if whitelist { + if pluginsState.sessionData == nil { + pluginsState.sessionData = make(map[string]interface{}) + } + pluginsState.sessionData["whitelisted"] = true + if plugin.logger != nil { + var clientIPStr string + if pluginsState.clientProto == "udp" { + clientIPStr = (*pluginsState.clientAddr).(*net.UDPAddr).IP.String() + } else { + clientIPStr = (*pluginsState.clientAddr).(*net.TCPAddr).IP.String() + } + var line string + if plugin.format == "tsv" { + now := time.Now() + year, month, day := now.Date() + hour, minute, second := now.Clock() + tsStr := fmt.Sprintf("[%d-%02d-%02d %02d:%02d:%02d]", year, int(month), day, hour, minute, second) + line = fmt.Sprintf("%s\t%s\t%s\t%s\n", tsStr, clientIPStr, StringQuote(qName), StringQuote(reason)) + } else if plugin.format == "ltsv" { + line = fmt.Sprintf("time:%d\thost:%s\tqname:%s\tmessage:%s\n", time.Now().Unix(), clientIPStr, StringQuote(qName), StringQuote(reason)) + } else { + dlog.Fatalf("Unexpected log format: [%s]", plugin.format) + } + if plugin.logger == nil { + return errors.New("Log file not initialized") + } + plugin.logger.Write([]byte(line)) + } + } + return nil +} diff --git a/dnscrypt-proxy/plugins.go b/dnscrypt-proxy/plugins.go index 031e8152..329dced4 100644 --- a/dnscrypt-proxy/plugins.go +++ b/dnscrypt-proxy/plugins.go @@ -45,6 +45,9 @@ func InitPluginsGlobals(pluginsGlobals *PluginsGlobals, proxy *Proxy) error { if len(proxy.queryLogFile) != 0 { *queryPlugins = append(*queryPlugins, Plugin(new(PluginQueryLog))) } + if len(proxy.whitelistNameFile) != 0 { + *queryPlugins = append(*queryPlugins, Plugin(new(PluginWhitelistName))) + } if len(proxy.blockNameFile) != 0 { *queryPlugins = append(*queryPlugins, Plugin(new(PluginBlockName))) } diff --git a/dnscrypt-proxy/proxy.go b/dnscrypt-proxy/proxy.go index 3b5cb5d9..93155ab3 100644 --- a/dnscrypt-proxy/proxy.go +++ b/dnscrypt-proxy/proxy.go @@ -38,8 +38,11 @@ type Proxy struct { nxLogFile string nxLogFormat string blockNameFile string + whitelistNameFile string blockNameLogFile string + whitelistNameLogFile string blockNameFormat string + whitelistNameFormat string blockIPFile string blockIPLogFile string blockIPFormat string