diff --git a/README.md b/README.md index 41409de9..79f63fa4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # ![dnscrypt-proxy 2](https://raw.github.com/jedisct1/dnscrypt-proxy/master/logo.png?2) -A modern client implementation of the [DNSCrypt](https://github.com/DNSCrypt/dnscrypt-protocol/blob/master/DNSCRYPT-V2-PROTOCOL.txt) protocol. +A flexible DNS proxy, with support for encrypted DNS protocols such as [DNSCrypt](https://github.com/DNSCrypt/dnscrypt-protocol/blob/master/DNSCRYPT-V2-PROTOCOL.txt). ## [dnscrypt-proxy 2.0.0beta6 is available for download!](https://github.com/jedisct1/dnscrypt-proxy/releases/latest) diff --git a/dnscrypt-proxy/config.go b/dnscrypt-proxy/config.go index c210fcad..fbfbed39 100644 --- a/dnscrypt-proxy/config.go +++ b/dnscrypt-proxy/config.go @@ -31,6 +31,7 @@ type Config struct { QueryLog QueryLogConfig `toml:"query_log"` NxLog NxLogConfig `toml:"nx_log"` BlockName BlockNameConfig `toml:"blacklist"` + BlockIP BlockIPConfig `toml:"ip_blacklist"` ForwardFile string `toml:"forwarding_rules"` ServersConfig map[string]ServerConfig `toml:"servers"` SourcesConfig map[string]SourceConfig `toml:"sources"` @@ -94,6 +95,12 @@ type BlockNameConfig struct { Format string `toml:"log_format"` } +type BlockIPConfig struct { + File string `toml:"blacklist_file"` + LogFile string `toml:"log_file"` + Format string `toml:"log_format"` +} + func ConfigLoad(proxy *Proxy, svcFlag *string, config_file string) error { version := flag.Bool("version", false, "prints current proxy version") configFile := flag.String("config", "dnscrypt-proxy.toml", "path to the configuration file") @@ -174,6 +181,18 @@ func ConfigLoad(proxy *Proxy, svcFlag *string, config_file string) error { proxy.blockNameFormat = config.BlockName.Format proxy.blockNameLogFile = config.BlockName.LogFile + if len(config.BlockIP.Format) == 0 { + config.BlockIP.Format = "tsv" + } else { + config.BlockIP.Format = strings.ToLower(config.BlockIP.Format) + } + if config.BlockIP.Format != "tsv" && config.BlockIP.Format != "ltsv" { + return errors.New("Unsupported IP block log format") + } + proxy.blockIPFile = config.BlockIP.File + proxy.blockIPFormat = config.BlockIP.Format + proxy.blockIPLogFile = config.BlockIP.LogFile + proxy.forwardFile = config.ForwardFile requiredProps := ServerInformalProperties(0) diff --git a/dnscrypt-proxy/dnscrypt-proxy.toml b/dnscrypt-proxy/dnscrypt-proxy.toml index acf0b5b8..2e4a5221 100644 --- a/dnscrypt-proxy/dnscrypt-proxy.toml +++ b/dnscrypt-proxy/dnscrypt-proxy.toml @@ -205,6 +205,34 @@ format = 'tsv' +########################################################### +# Pattern-based IP blocking (IP blacklists) # +########################################################### + +## IP blacklists are made of one pattern per line. Example of valid patterns: +## +## 127.* +## fe80:abcd:* +## 192.168.1.4 + +[ip_blacklist] + +## Path to the file of blocking rules (absolute, or relative to the same directory as the executable file) + +# blacklist_file = 'ip-blacklist.txt' + + +## Optional path to a file logging blocked queries + +# log_file = 'ip-blocked.log' + + +## Optional log format: tsv or ltsv (default: tsv) + +# log_format = 'tsv' + + + ######################### # Servers # ######################### diff --git a/dnscrypt-proxy/main.go b/dnscrypt-proxy/main.go index b7f36595..8e9ac79d 100644 --- a/dnscrypt-proxy/main.go +++ b/dnscrypt-proxy/main.go @@ -44,6 +44,9 @@ type Proxy struct { blockNameFile string blockNameLogFile string blockNameFormat string + blockIPFile string + blockIPLogFile string + blockIPFormat string forwardFile string pluginsGlobals PluginsGlobals urlsToPrefetch []URLToPrefetch diff --git a/dnscrypt-proxy/plugin_block_ip.go b/dnscrypt-proxy/plugin_block_ip.go new file mode 100644 index 00000000..4b7d10d2 --- /dev/null +++ b/dnscrypt-proxy/plugin_block_ip.go @@ -0,0 +1,165 @@ +package main + +import ( + "errors" + "fmt" + "io/ioutil" + "net" + "os" + "strings" + "sync" + "time" + "unicode" + + "github.com/hashicorp/go-immutable-radix" + "github.com/jedisct1/dlog" + "github.com/miekg/dns" +) + +type PluginBlockIP struct { + sync.Mutex + blockedPrefixes *iradix.Tree + blockedIPs map[string]interface{} + outFd *os.File + format string +} + +func (plugin *PluginBlockIP) Name() string { + return "block_ip" +} + +func (plugin *PluginBlockIP) Description() string { + return "Block responses containing specific IP addresses" +} + +func (plugin *PluginBlockIP) Init(proxy *Proxy) error { + dlog.Noticef("Loading the set of IP blocking rules from [%s]", proxy.blockIPFile) + bin, err := ioutil.ReadFile(proxy.blockIPFile) + if err != nil { + return err + } + plugin.blockedPrefixes = iradix.New() + plugin.blockedIPs = make(map[string]interface{}) + for lineNo, line := range strings.Split(string(bin), "\n") { + line = strings.TrimFunc(line, unicode.IsSpace) + if len(line) == 0 || strings.HasPrefix(line, "#") { + continue + } + ip := net.ParseIP(line) + trailingStar := strings.HasSuffix(line, "*") + if len(line) < 2 || (ip != nil && trailingStar) { + dlog.Errorf("Suspicious IP blocking 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 IP blocking 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.blockedPrefixes, _, _ = plugin.blockedPrefixes.Insert([]byte(line), 0) + } else { + plugin.blockedIPs[line] = true + } + } + if len(proxy.blockIPLogFile) == 0 { + return nil + } + plugin.Lock() + defer plugin.Unlock() + outFd, err := os.OpenFile(proxy.blockIPLogFile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644) + if err != nil { + return err + } + plugin.outFd = outFd + plugin.format = proxy.blockIPFormat + + return nil +} + +func (plugin *PluginBlockIP) Drop() error { + return nil +} + +func (plugin *PluginBlockIP) Reload() error { + return nil +} + +func (plugin *PluginBlockIP) Eval(pluginsState *PluginsState, msg *dns.Msg) error { + answers := msg.Answer + if len(answers) == 0 { + return nil + } + reject, 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.blockedIPs[ipStr]; found { + reject, reason = true, ipStr + break + } + match, _, found := plugin.blockedPrefixes.Root().LongestPrefix([]byte(ipStr)) + if found { + if len(match) == len(ipStr) || (ipStr[len(match)] == '.' || ipStr[len(match)] == ':') { + reject, reason = true, string(match)+"*" + break + } + } + } + if reject { + pluginsState.action = PluginsActionReject + if plugin.outFd != nil { + questions := msg.Question + if len(questions) != 1 { + return nil + } + qName := strings.ToLower(StripTrailingDot(questions[0].Name)) + if len(qName) < 2 { + return 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\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) + } + plugin.Lock() + if plugin.outFd == nil { + return errors.New("Log file not initialized") + } + plugin.outFd.WriteString(line) + defer plugin.Unlock() + } + } + return nil +} diff --git a/dnscrypt-proxy/plugin_block_name.go b/dnscrypt-proxy/plugin_block_name.go index 79b75354..70eef85c 100644 --- a/dnscrypt-proxy/plugin_block_name.go +++ b/dnscrypt-proxy/plugin_block_name.go @@ -118,6 +118,7 @@ func (plugin *PluginBlockName) Init(proxy *Proxy) error { } plugin.outFd = outFd plugin.format = proxy.blockNameFormat + return nil } diff --git a/dnscrypt-proxy/plugins.go b/dnscrypt-proxy/plugins.go index f3a3b79b..e8c9593c 100644 --- a/dnscrypt-proxy/plugins.go +++ b/dnscrypt-proxy/plugins.go @@ -60,13 +60,15 @@ func InitPluginsGlobals(pluginsGlobals *PluginsGlobals, proxy *Proxy) error { } responsePlugins := &[]Plugin{} - if proxy.cache { - *responsePlugins = append(*responsePlugins, Plugin(new(PluginCacheResponse))) - } if len(proxy.nxLogFile) != 0 { *responsePlugins = append(*responsePlugins, Plugin(new(PluginNxLog))) } - + if len(proxy.blockIPFile) != 0 { + *responsePlugins = append(*responsePlugins, Plugin(new(PluginBlockIP))) + } + if proxy.cache { + *responsePlugins = append(*responsePlugins, Plugin(new(PluginCacheResponse))) + } for _, plugin := range *queryPlugins { if err := plugin.Init(proxy); err != nil { return err