From 90a9a9d9923677233ed2210788a5beaae46b162c Mon Sep 17 00:00:00 2001 From: Ian Bashford Date: Sun, 15 Nov 2020 19:59:58 +0000 Subject: [PATCH] allowed ips plugin (#1510) --- dnscrypt-proxy/config.go | 19 +++ dnscrypt-proxy/example-allowed-ips.txt | 7 + dnscrypt-proxy/example-dnscrypt-proxy.toml | 27 ++++ dnscrypt-proxy/plugin_allow_ip.go | 146 +++++++++++++++++++++ dnscrypt-proxy/plugins.go | 3 + dnscrypt-proxy/proxy.go | 3 + 6 files changed, 205 insertions(+) create mode 100644 dnscrypt-proxy/example-allowed-ips.txt create mode 100644 dnscrypt-proxy/plugin_allow_ip.go diff --git a/dnscrypt-proxy/config.go b/dnscrypt-proxy/config.go index 086cd011..7966cfda 100644 --- a/dnscrypt-proxy/config.go +++ b/dnscrypt-proxy/config.go @@ -67,6 +67,7 @@ type Config struct { AllowedName AllowedNameConfig `toml:"allowed_names"` BlockIP BlockIPConfig `toml:"blocked_ips"` BlockIPLegacy BlockIPConfigLegacy `toml:"ip_blacklist"` + AllowIP AllowIPConfig `toml:"allowed_ips"` ForwardFile string `toml:"forwarding_rules"` CloakFile string `toml:"cloaking_rules"` CaptivePortalFile string `toml:"captive_portal_handler"` @@ -213,6 +214,12 @@ type BlockIPConfigLegacy struct { Format string `toml:"log_format"` } +type AllowIPConfig struct { + File string `toml:"allowed_ips_file"` + LogFile string `toml:"log_file"` + Format string `toml:"log_format"` +} + type AnonymizedDNSRouteConfig struct { ServerName string `toml:"server_name"` RelayNames []string `toml:"via"` @@ -558,6 +565,18 @@ func ConfigLoad(proxy *Proxy, flags *ConfigFlags) error { proxy.blockIPFormat = config.BlockIP.Format proxy.blockIPLogFile = config.BlockIP.LogFile + if len(config.AllowIP.Format) == 0 { + config.AllowIP.Format = "tsv" + } else { + config.AllowIP.Format = strings.ToLower(config.AllowIP.Format) + } + if config.AllowIP.Format != "tsv" && config.AllowIP.Format != "ltsv" { + return errors.New("Unsupported allowed_ips log format") + } + proxy.allowedIPFile = config.AllowIP.File + proxy.allowedIPFormat = config.AllowIP.Format + proxy.allowedIPLogFile = config.AllowIP.LogFile + proxy.forwardFile = config.ForwardFile proxy.cloakFile = config.CloakFile proxy.captivePortalFile = config.CaptivePortalFile diff --git a/dnscrypt-proxy/example-allowed-ips.txt b/dnscrypt-proxy/example-allowed-ips.txt new file mode 100644 index 00000000..e7e99e27 --- /dev/null +++ b/dnscrypt-proxy/example-allowed-ips.txt @@ -0,0 +1,7 @@ +############################## +# Allowed IPs List # +############################## + +#192.168.0.* +#fe80:53:* # IPv6 prefix example +#81.169.145.105 diff --git a/dnscrypt-proxy/example-dnscrypt-proxy.toml b/dnscrypt-proxy/example-dnscrypt-proxy.toml index 54a52fdc..dee5459a 100644 --- a/dnscrypt-proxy/example-dnscrypt-proxy.toml +++ b/dnscrypt-proxy/example-dnscrypt-proxy.toml @@ -545,6 +545,33 @@ cache_neg_max_ttl = 600 +######################################################### +# Pattern-based allowed IPs lists (blocklists bypass) # +######################################################### + +## Allowed IP lists support the same patterns as IP blocklists +## If an IP response matches an allow ip entry, the corresponding session +## will bypass IP filters. +## +## Time-based rules are also supported to make some websites only accessible at specific times of the day. + +[allowed_ips] + + ## Path to the file of allowed ip rules (absolute, or relative to the same directory as the config file) + + # allowed_ips_file = 'allowed-ips.txt' + + + ## Optional path to a file logging allowed queries + + # log_file = 'allowed-ips.log' + + ## Optional log format: tsv or ltsv (default: tsv) + + # log_format = 'tsv' + + + ########################################## # Time access restrictions # ########################################## diff --git a/dnscrypt-proxy/plugin_allow_ip.go b/dnscrypt-proxy/plugin_allow_ip.go new file mode 100644 index 00000000..8f98a726 --- /dev/null +++ b/dnscrypt-proxy/plugin_allow_ip.go @@ -0,0 +1,146 @@ +package main + +import ( + "errors" + "fmt" + "io" + "net" + "strings" + "time" + + iradix "github.com/hashicorp/go-immutable-radix" + "github.com/jedisct1/dlog" + "github.com/miekg/dns" +) + +type PluginAllowedIP struct { + allowedPrefixes *iradix.Tree + allowedIPs map[string]interface{} + logger io.Writer + format string +} + +func (plugin *PluginAllowedIP) Name() string { + return "allow_ip" +} + +func (plugin *PluginAllowedIP) Description() string { + return "Allows DNS queries containing specific IP addresses" +} + +func (plugin *PluginAllowedIP) Init(proxy *Proxy) error { + dlog.Noticef("Loading the set of allowed IP rules from [%s]", proxy.allowedIPFile) + bin, err := ReadTextFile(proxy.allowedIPFile) + if err != nil { + return err + } + plugin.allowedPrefixes = iradix.New() + plugin.allowedIPs = make(map[string]interface{}) + for lineNo, line := range strings.Split(string(bin), "\n") { + line = TrimAndStripInlineComments(line) + if len(line) == 0 { + continue + } + ip := net.ParseIP(line) + trailingStar := strings.HasSuffix(line, "*") + if len(line) < 2 || (ip != nil && trailingStar) { + dlog.Errorf("Suspicious allowed IP rule [%s] at line %d", line, lineNo) + continue + } + if trailingStar { + line = line[:len(line)-1] + } + if strings.HasSuffix(line, ":") || strings.HasSuffix(line, ".") { + line = line[:len(line)-1] + } + if len(line) == 0 { + dlog.Errorf("Empty allowed IP rule at line %d", lineNo) + continue + } + if strings.Contains(line, "*") { + dlog.Errorf("Invalid rule: [%s] - wildcards can only be used as a suffix at line %d", line, lineNo) + continue + } + line = strings.ToLower(line) + if trailingStar { + plugin.allowedPrefixes, _, _ = plugin.allowedPrefixes.Insert([]byte(line), 0) + } else { + plugin.allowedIPs[line] = true + } + } + if len(proxy.allowedIPLogFile) == 0 { + return nil + } + plugin.logger = Logger(proxy.logMaxSize, proxy.logMaxAge, proxy.logMaxBackups, proxy.allowedIPLogFile) + plugin.format = proxy.allowedIPFormat + + return nil +} + +func (plugin *PluginAllowedIP) Drop() error { + return nil +} + +func (plugin *PluginAllowedIP) Reload() error { + return nil +} + +func (plugin *PluginAllowedIP) Eval(pluginsState *PluginsState, msg *dns.Msg) error { + answers := msg.Answer + if len(answers) == 0 { + return nil + } + allowed, reason, ipStr := false, "", "" + for _, answer := range answers { + header := answer.Header() + Rrtype := header.Rrtype + if header.Class != dns.ClassINET || (Rrtype != dns.TypeA && Rrtype != dns.TypeAAAA) { + continue + } + if Rrtype == dns.TypeA { + ipStr = answer.(*dns.A).A.String() + } else if Rrtype == dns.TypeAAAA { + ipStr = answer.(*dns.AAAA).AAAA.String() // IPv4-mapped IPv6 addresses are converted to IPv4 + } + if _, found := plugin.allowedIPs[ipStr]; found { + allowed, reason = true, ipStr + break + } + match, _, found := plugin.allowedPrefixes.Root().LongestPrefix([]byte(ipStr)) + if found { + if len(match) == len(ipStr) || (ipStr[len(match)] == '.' || ipStr[len(match)] == ':') { + allowed, reason = true, string(match)+"*" + break + } + } + } + if allowed { + pluginsState.sessionData["whitelisted"] = true + if plugin.logger != nil { + qName := pluginsState.qName + 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\t%s\n", tsStr, clientIPStr, StringQuote(qName), StringQuote(ipStr), StringQuote(reason)) + } else if plugin.format == "ltsv" { + line = fmt.Sprintf("time:%d\thost:%s\tqname:%s\tip:%s\tmessage:%s\n", time.Now().Unix(), clientIPStr, StringQuote(qName), StringQuote(ipStr), 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 a69d4efd..bfb66d7a 100644 --- a/dnscrypt-proxy/plugins.go +++ b/dnscrypt-proxy/plugins.go @@ -132,6 +132,9 @@ func (proxy *Proxy) InitPluginsGlobals() error { if len(proxy.nxLogFile) != 0 { *responsePlugins = append(*responsePlugins, Plugin(new(PluginNxLog))) } + if len(proxy.allowedIPFile) != 0 { + *responsePlugins = append(*responsePlugins, Plugin(new(PluginAllowedIP))) + } if len(proxy.blockNameFile) != 0 { *responsePlugins = append(*responsePlugins, Plugin(new(PluginBlockNameResponse))) } diff --git a/dnscrypt-proxy/proxy.go b/dnscrypt-proxy/proxy.go index 51877f8d..a50d4d69 100644 --- a/dnscrypt-proxy/proxy.go +++ b/dnscrypt-proxy/proxy.go @@ -48,6 +48,9 @@ type Proxy struct { forwardFile string blockIPFormat string blockIPLogFile string + allowedIPFile string + allowedIPFormat string + allowedIPLogFile string queryLogFormat string blockIPFile string whitelistNameFormat string