From 96dadc7aca860a2b16e8e2bcde3ebad44f378031 Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Wed, 17 Jan 2018 09:44:03 +0100 Subject: [PATCH] Forwarding plugin --- README.md | 49 ++++++------- dnscrypt-proxy/config.go | 2 + dnscrypt-proxy/dnscrypt-proxy.toml | 50 +++++++------ dnscrypt-proxy/main.go | 1 + dnscrypt-proxy/plugin_block_name.go | 10 ++- dnscrypt-proxy/plugin_forward.go | 105 ++++++++++++++++++++++++++++ dnscrypt-proxy/plugins.go | 4 +- dnscrypt-proxy/sources.go | 2 +- 8 files changed, 170 insertions(+), 53 deletions(-) create mode 100644 dnscrypt-proxy/plugin_forward.go diff --git a/README.md b/README.md index 7eefa2e4..aa6d57e6 100644 --- a/README.md +++ b/README.md @@ -8,30 +8,31 @@ A modern client implementation of the [DNSCrypt](https://github.com/DNSCrypt/dns ## Current status/features -| Features | dnscrypt-proxy 1.x | dnscrypt-proxy 2.x | -| ---------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------- | -| Status | Old PoC, barely maintained any more | Very new, but quickly evolving | -| Code quality | Big ugly mess | Readable, easy to work on | -| Reliability | Poor, due to completely broken handling of edge cases | Excellent | -| Security | Written in C, bundles patched versions from old branches of system libraries | Written in standard and portable Go | -| Dependencies | Specific versions of dnscrypt-proxy, libldns and libtool | None | -| Upstream connections using TCP | Catastrophic, requires client retries | Implemented as anyone would expect, works well with TOR | -| XChaCha20 support | Only if compiled with recent versions of libsodium | Yes, always available | -| Support of links with small MTU | Unreliable due to completely broken padding | Reliable, properly implemented | -| Support for multiple servers | Nonexistent | Yes, with automatic failover and load-balancing | -| Custom additions | C API, requires libldns for sanity | Simple Go structures using miekg/dns | -| AAAA blocking for IPv4-only networks | Yes | Yes | -| DNS caching | Yes, with ugly hacks for DNSSEC support | Yes, without ugly hacks | -| EDNS support | Broken with custom records | Yes | -| Asynchronous filters | Lol, no, filters block everything | Of course, thanks to Go | -| Session-local storage for extensions | Impossible | Yes | -| Multicore support | Nonexistent | Yes, thanks to Go | -| Efficient padding of queries | Couldn't be any worse | Yes | -| Multiple local sockets | Impossible | Of course. IPv4, IPv6, as many as you like | -| Automatically picks the fastest servers | Lol, it supports only one at a time, anyway | Yes, out of the box | -| Official, always up-to-date pre-built libraries | None | Yes, for many platforms. See below. | -| Automatically downloads and verifies servers lists | No. Requires custom scripts, cron jobs and dependencies (minisign) | Yes, built-in, including signature verification | -| Advanced expresions in blacklists (ads*.example[0-9]*.com) | No | Yes | +| Features | dnscrypt-proxy 1.x | dnscrypt-proxy 2.x | +| ----------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------- | +| Status | Old PoC, barely maintained any more | Very new, but quickly evolving | +| Code quality | Big ugly mess | Readable, easy to work on | +| Reliability | Poor, due to completely broken handling of edge cases | Excellent | +| Security | Written in C, bundles patched versions from old branches of system libraries | Written in standard and portable Go | +| Dependencies | Specific versions of dnscrypt-proxy, libldns and libtool | None | +| Upstream connections using TCP | Catastrophic, requires client retries | Implemented as anyone would expect, works well with TOR | +| XChaCha20 support | Only if compiled with recent versions of libsodium | Yes, always available | +| Support of links with small MTU | Unreliable due to completely broken padding | Reliable, properly implemented | +| Support for multiple servers | Nonexistent | Yes, with automatic failover and load-balancing | +| Custom additions | C API, requires libldns for sanity | Simple Go structures using miekg/dns | +| AAAA blocking for IPv4-only networks | Yes | Yes | +| DNS caching | Yes, with ugly hacks for DNSSEC support | Yes, without ugly hacks | +| EDNS support | Broken with custom records | Yes | +| Asynchronous filters | Lol, no, filters block everything | Of course, thanks to Go | +| Session-local storage for extensions | Impossible | Yes | +| Multicore support | Nonexistent | Yes, thanks to Go | +| Efficient padding of queries | Couldn't be any worse | Yes | +| Multiple local sockets | Impossible | Of course. IPv4, IPv6, as many as you like | +| Automatically picks the fastest servers | Lol, it supports only one at a time, anyway | Yes, out of the box | +| Official, always up-to-date pre-built libraries | None | Yes, for many platforms. See below. | +| Automatically downloads and verifies servers lists | No. Requires custom scripts, cron jobs and dependencies (minisign) | Yes, built-in, including signature verification | +| Advanced expressions in blacklists (ads*.example[0-9]*.com) | No | Yes | +| Forwarding with load balancing | No | Yes | ## Planned features diff --git a/dnscrypt-proxy/config.go b/dnscrypt-proxy/config.go index ca9031c4..19af4b47 100644 --- a/dnscrypt-proxy/config.go +++ b/dnscrypt-proxy/config.go @@ -26,6 +26,7 @@ type Config struct { CacheMaxTTL uint32 `toml:"cache_max_ttl"` QueryLog QueryLogConfig `toml:"query_log"` BlockName BlockNameConfig `toml:"block_name"` + ForwardFile string `toml:"forwarding_rules"` ServersConfig map[string]ServerConfig `toml:"servers"` SourcesConfig map[string]SourceConfig `toml:"sources"` } @@ -104,6 +105,7 @@ func ConfigLoad(proxy *Proxy, config_file string) error { proxy.queryLogFile = config.QueryLog.File proxy.queryLogFormat = config.QueryLog.Format proxy.blockNameFile = config.BlockName.File + proxy.forwardFile = config.ForwardFile if len(config.ServerNames) == 0 { for serverName := range config.ServersConfig { config.ServerNames = append(config.ServerNames, serverName) diff --git a/dnscrypt-proxy/dnscrypt-proxy.toml b/dnscrypt-proxy/dnscrypt-proxy.toml index 411a1051..261e14f7 100644 --- a/dnscrypt-proxy/dnscrypt-proxy.toml +++ b/dnscrypt-proxy/dnscrypt-proxy.toml @@ -48,28 +48,12 @@ cert_refresh_delay = 30 block_ipv6 = false -############## Query logging ############## +############## Route queries for specific domains to a dedicated set of servers ############## +# Example map entries (one entry per line): +# example.com: 9.9.9.9 +# example.net: 9.9.9.9,8.8.8.8 -## Log client queries to a file - -[query_log] -### Full path to the query log file -# file = "query.log" - -### Query log format (currently supported: tsv and ltsv) -format = "tsv" - - -############## Pattern-based blocking (blacklists) ############## -# Blacklists are made of one pattern per line. Example of valid patterns: -# example.com -# *sex* -# ads.* -# ads*.example.* -# ads*.example[0-9]*.com -[block_name] -## Full path to the file of blocking rules -file = "blacklist.txt" +forwarding_rules = "forwarding-rules.txt" ############## DNS Cache ############## @@ -99,6 +83,30 @@ cache_max_ttl = 86400 cache_neg_ttl = 60 +############## Query logging ############## +# Log client queries to a file + +[query_log] +### Full path to the query log file +# file = "query.log" + +### Query log format (currently supported: tsv and ltsv) +format = "tsv" + + +############## Pattern-based blocking (blacklists) ############## +# Blacklists are made of one pattern per line. Example of valid patterns: +# example.com +# *sex* +# ads.* +# ads*.example.* +# ads*.example[0-9]*.com + +[block_name] +## Full path to the file of blocking rules +file = "blacklist.txt" + + ############## Servers ############## ## Remote lists of available servers diff --git a/dnscrypt-proxy/main.go b/dnscrypt-proxy/main.go index f70d8f14..23b68421 100644 --- a/dnscrypt-proxy/main.go +++ b/dnscrypt-proxy/main.go @@ -31,6 +31,7 @@ type Proxy struct { queryLogFile string queryLogFormat string blockNameFile string + forwardFile string pluginsGlobals PluginsGlobals } diff --git a/dnscrypt-proxy/plugin_block_name.go b/dnscrypt-proxy/plugin_block_name.go index da479bd8..1bfa1dca 100644 --- a/dnscrypt-proxy/plugin_block_name.go +++ b/dnscrypt-proxy/plugin_block_name.go @@ -1,7 +1,6 @@ package main import ( - "fmt" "io/ioutil" "path/filepath" "strings" @@ -54,23 +53,22 @@ func (plugin *PluginBlockName) Init(proxy *Proxy) error { blockType := PluginBlockTypeNone if isGlobCandidate(line) { blockType = PluginBlockTypePattern - fmt.Println(line) _, err := filepath.Match(line, "example.com") if len(line) < 2 || err != nil { - dlog.Errorf("Syntax error in block rules at line %d", lineNo) + dlog.Errorf("Syntax error in block rules at line %d", 1+lineNo) continue } } else if leadingStar && trailingStar { blockType = PluginBlockTypeSubstring if len(line) < 3 { - dlog.Errorf("Syntax error in block rules at line %d", lineNo) + dlog.Errorf("Syntax error in block rules at line %d", 1+lineNo) continue } line = line[1 : len(line)-1] } else if trailingStar { blockType = PluginBlockTypePrefix if len(line) < 2 { - dlog.Errorf("Syntax error in block rules at line %d", lineNo) + dlog.Errorf("Syntax error in block rules at line %d", 1+lineNo) continue } line = line[:len(line)-1] @@ -84,7 +82,7 @@ func (plugin *PluginBlockName) Init(proxy *Proxy) error { } } if len(line) == 0 { - dlog.Errorf("Syntax error in block rule at line %d", lineNo) + dlog.Errorf("Syntax error in block rule at line %d", 1+lineNo) continue } line = strings.ToLower(line) diff --git a/dnscrypt-proxy/plugin_forward.go b/dnscrypt-proxy/plugin_forward.go new file mode 100644 index 00000000..1c5a6800 --- /dev/null +++ b/dnscrypt-proxy/plugin_forward.go @@ -0,0 +1,105 @@ +package main + +import ( + "fmt" + "io/ioutil" + "math/rand" + "net" + "strings" + + "github.com/jedisct1/dlog" + "github.com/miekg/dns" +) + +type PluginForwardEntry struct { + domain string + servers []string +} + +type PluginForward struct { + forwardMap []PluginForwardEntry +} + +func (plugin *PluginForward) Name() string { + return "forward" +} + +func (plugin *PluginForward) Description() string { + return "Route queries matching specific domains to a dedicated set of servers" +} + +func (plugin *PluginForward) Init(proxy *Proxy) error { + dlog.Noticef("Loading the set of forwarding rules from [%s]", proxy.forwardFile) + bin, err := ioutil.ReadFile(proxy.forwardFile) + if err != nil { + return err + } + for lineNo, line := range strings.Split(string(bin), "\n") { + parts := strings.SplitN(line, ":", 2) + if len(parts) == 0 || len(strings.Trim(parts[0], " \t\r")) == 0 { + continue + } + if len(parts) != 2 { + return fmt.Errorf("Syntax error for a forwarding rule at line %d. Expected syntax: example.com: 9.9.9.9,8.8.8.8", 1+lineNo) + } + domain := strings.ToLower(strings.Trim(parts[0], " \t\r")) + serversStr := strings.Trim(parts[1], " \t\r") + if len(domain) == 0 || len(serversStr) == 0 { + continue + } + var servers []string + for _, server := range strings.Split(serversStr, ",") { + server = strings.Trim(server, " \t\r") + if net.ParseIP(server) != nil { + server = fmt.Sprintf("%s:%d", server, 53) + } + servers = append(servers, server) + } + if len(servers) == 0 { + continue + } + plugin.forwardMap = append(plugin.forwardMap, PluginForwardEntry{ + domain: domain, servers: servers, + }) + } + return nil +} + +func (plugin *PluginForward) Drop() error { + return nil +} + +func (plugin *PluginForward) Reload() error { + return nil +} + +func (plugin *PluginForward) Eval(pluginsState *PluginsState, msg *dns.Msg) error { + questions := msg.Question + if len(questions) != 1 { + return nil + } + question := strings.ToLower(StripTrailingDot(questions[0].Name)) + questionLen := len(question) + var servers []string + for _, candidate := range plugin.forwardMap { + candidateLen := len(candidate.domain) + if candidateLen > questionLen { + continue + } + if question[questionLen-candidateLen:] == candidate.domain && (candidateLen == questionLen || (question[questionLen-candidateLen] == '.')) { + servers = candidate.servers + break + } + } + if len(servers) == 0 { + return nil + } + server := servers[rand.Intn(len(servers))] + respMsg, err := dns.Exchange(msg, server) + if err != nil { + return err + } + pluginsState.synthResponse = respMsg + pluginsState.action = PluginsActionSynth + return nil +} diff --git a/dnscrypt-proxy/plugins.go b/dnscrypt-proxy/plugins.go index 92f7d71d..09433e81 100644 --- a/dnscrypt-proxy/plugins.go +++ b/dnscrypt-proxy/plugins.go @@ -55,7 +55,9 @@ func InitPluginsGlobals(pluginsGlobals *PluginsGlobals, proxy *Proxy) error { if proxy.cache { *queryPlugins = append(*queryPlugins, Plugin(new(PluginCache))) } - + if len(proxy.forwardFile) != 0 { + *queryPlugins = append(*queryPlugins, Plugin(new(PluginForward))) + } responsePlugins := &[]Plugin{} if proxy.cache { *responsePlugins = append(*responsePlugins, Plugin(new(PluginCacheResponse))) diff --git a/dnscrypt-proxy/sources.go b/dnscrypt-proxy/sources.go index fd2c6c1a..02130470 100644 --- a/dnscrypt-proxy/sources.go +++ b/dnscrypt-proxy/sources.go @@ -136,7 +136,7 @@ func (source *Source) Parse() ([]RegisteredServer, error) { continue } if len(record) < 14 { - return registeredServers, fmt.Errorf("Parse error at line %d", lineNo) + return registeredServers, fmt.Errorf("Parse error at line %d", 1+lineNo) } if lineNo == 0 { continue