From 518f0ce17d32679f1fec53960f50e0dcca1d6df7 Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Wed, 25 Sep 2019 15:51:13 +0200 Subject: [PATCH] Implement support for server-side blacklists --- README.md | 4 +++ encrypted-dns.toml | 10 ++++++ src/blacklist.rs | 76 ++++++++++++++++++++++++++++++++++++++++++++++ src/config.rs | 6 ++++ src/dns.rs | 15 +++++++++ src/globals.rs | 2 ++ src/main.rs | 12 +++++++- src/resolver.rs | 10 ++++-- 8 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 src/blacklist.rs diff --git a/README.md b/README.md index d8dd590..ea72415 100644 --- a/README.md +++ b/README.md @@ -89,3 +89,7 @@ The proxy creates and updates a file named `encrypted-dns.state` by default. Tha Do not delete the file, unless you want to change parameters (such as the provider name), and keep it secret, or the keys will be lost. Putting it in a directory that is only readable by the super-user is not a bad idea. + +## Filtering + +Domains can be filtered directly by the proxy, see the `[filtering]` section of the configuration file. diff --git a/encrypted-dns.toml b/encrypted-dns.toml index b95fd1a..1374185 100644 --- a/encrypted-dns.toml +++ b/encrypted-dns.toml @@ -150,3 +150,13 @@ key_cache_capacity = 10000 ## Where to prooxy TLS connections to (e.g. DoH server) # upstream_addr = "127.0.0.1:4343" + + + +####################################### +# Server-side filtering # +####################################### + +[filtering] + +# domain_blacklist = "/etc/domain_blacklist.txt" diff --git a/src/blacklist.rs b/src/blacklist.rs new file mode 100644 index 0000000..4b1669c --- /dev/null +++ b/src/blacklist.rs @@ -0,0 +1,76 @@ +use crate::errors::*; + +use std::collections::HashMap; +use std::fs::File; +use std::io::{BufRead, BufReader}; +use std::path::Path; +use std::sync::Arc; + +const MAX_ITERATIONS: usize = 5; + +#[derive(Debug)] +struct BlackListInner { + map: HashMap, ()>, +} + +#[derive(Clone, Debug)] +pub struct BlackList { + inner: Arc, + max_iterations: usize, +} + +impl BlackList { + pub fn new(map: HashMap, ()>, max_iterations: usize) -> Self { + let inner = Arc::new(BlackListInner { map }); + BlackList { + inner, + max_iterations, + } + } + + pub fn load>(path: P) -> Result { + let mut map = HashMap::new(); + let fp = BufReader::new(File::open(path)?); + for (line_nb, line) in fp.lines().enumerate() { + let line = line?; + let mut line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + while line.starts_with("*.") { + line = &line[2..]; + } + while line.ends_with('.') { + line = &line[..line.len() - 1]; + } + let qname = line.as_bytes().to_vec().to_ascii_lowercase(); + if qname.is_empty() { + bail!(format_err!("Unexpected blacklist rule at line {}", line_nb)) + } + map.insert(qname, ()); + } + Ok(BlackList::new(map, MAX_ITERATIONS)) + } + + pub fn find(&self, qname: &[u8]) -> bool { + let qname = qname.to_vec().to_ascii_lowercase(); + let mut qname = qname.as_slice(); + let map = &self.inner.map; + let mut iterations = self.max_iterations; + while qname.len() >= 4 && iterations > 0 { + if map.contains_key(qname) { + return true; + } + let mut it = qname.splitn(2, |x| *x == b'.'); + if it.next().is_none() { + break; + } + qname = match it.next() { + None => break, + Some(qname) => qname, + }; + iterations -= 1; + } + false + } +} diff --git a/src/config.rs b/src/config.rs index a53bd1e..dc36c8f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -29,6 +29,11 @@ pub struct ListenAddrConfig { pub external: SocketAddr, } +#[derive(Serialize, Deserialize, Debug)] +pub struct FilteringConfig { + pub domain_blacklist: Option, +} + #[derive(Serialize, Deserialize, Debug)] pub struct Config { pub listen_addrs: Vec, @@ -46,6 +51,7 @@ pub struct Config { pub user: Option, pub group: Option, pub chroot: Option, + pub filtering: FilteringConfig, pub dnscrypt: DNSCryptConfig, pub tls: TLSConfig, pub daemonize: bool, diff --git a/src/dns.rs b/src/dns.rs index 817c6cf..5e1666c 100644 --- a/src/dns.rs +++ b/src/dns.rs @@ -372,3 +372,18 @@ pub fn serve_truncated(client_packet: Vec) -> Result, Error> { truncate(&mut packet); Ok(packet) } + +pub fn serve_empty_response(client_packet: Vec) -> Result, Error> { + ensure!(client_packet.len() >= DNS_HEADER_SIZE, "Short packet"); + ensure!(qdcount(&client_packet) == 1, "No question"); + ensure!( + !is_response(&client_packet), + "Question expected, but got a response instead" + ); + let offset = skip_name(&client_packet, DNS_HEADER_SIZE)?; + let mut packet = client_packet; + ensure!(packet.len() - offset >= 4, "Short packet"); + packet.truncate(offset + 4); + authoritative_response(&mut packet); + Ok(packet) +} diff --git a/src/globals.rs b/src/globals.rs index fe3a073..4f96ea8 100644 --- a/src/globals.rs +++ b/src/globals.rs @@ -1,3 +1,4 @@ +use crate::blacklist::*; use crate::cache::*; use crate::crypto::*; use crate::dnscrypt_certs::*; @@ -35,4 +36,5 @@ pub struct Globals { pub key_cache_capacity: usize, pub hasher: SipHasher13, pub cache: Cache, + pub blacklist: Option, } diff --git a/src/main.rs b/src/main.rs index 21f77dd..960e9db 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ #![allow(clippy::assertions_on_constants)] #![allow(clippy::type_complexity)] +#![allow(clippy::cognitive_complexity)] #![allow(dead_code)] #[global_allocator] @@ -18,6 +19,7 @@ extern crate serde_derive; #[macro_use] extern crate serde_big_array; +mod blacklist; mod cache; mod config; mod crypto; @@ -28,6 +30,7 @@ mod errors; mod globals; mod resolver; +use blacklist::*; use cache::*; use config::*; use crypto::*; @@ -521,7 +524,13 @@ fn main() -> Result<(), Error> { config.cache_ttl_max, config.cache_ttl_error, ); - + let blacklist = match config.filtering.domain_blacklist { + None => None, + Some(path) => Some( + BlackList::load(&path) + .map_err(|e| format_err!("Unable to load the blacklist [{:?}]: [{}]", path, e))?, + ), + }; let globals = Arc::new(Globals { runtime: runtime.clone(), state_file: state_file.to_path_buf(), @@ -549,6 +558,7 @@ fn main() -> Result<(), Error> { key_cache_capacity, hasher, cache, + blacklist, }); let updater = DNSCryptEncryptionParamsUpdater::new(globals.clone()); if !state_is_new { diff --git a/src/resolver.rs b/src/resolver.rs index 55bb26e..5e436e0 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -13,6 +13,12 @@ use tokio::prelude::*; use tokio_net::driver::Handle; pub async fn resolve(globals: &Globals, mut packet: &mut Vec) -> Result, Error> { + let packet_qname = dns::qname(&packet)?; + if let Some(blacklist) = &globals.blacklist { + if blacklist.find(&packet_qname) { + return dns::serve_empty_response(packet.to_vec()); + } + } let original_tid = dns::tid(&packet); dns::set_tid(&mut packet, 0); let mut hasher = globals.hasher; @@ -59,7 +65,7 @@ pub async fn resolve(globals: &Globals, mut packet: &mut Vec) -> Result= DNS_HEADER_SIZE && dns::tid(&response) == tid - && dns::qname(&packet)? == dns::qname(&response)? + && packet_qname == dns::qname(&response)? { break; } @@ -98,7 +104,7 @@ pub async fn resolve(globals: &Globals, mut packet: &mut Vec) -> Result