Implement support for server-side blacklists

pull/5/head
Frank Denis 5 years ago
parent 80e70eace4
commit 518f0ce17d

@ -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.

@ -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"

@ -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<Vec<u8>, ()>,
}
#[derive(Clone, Debug)]
pub struct BlackList {
inner: Arc<BlackListInner>,
max_iterations: usize,
}
impl BlackList {
pub fn new(map: HashMap<Vec<u8>, ()>, max_iterations: usize) -> Self {
let inner = Arc::new(BlackListInner { map });
BlackList {
inner,
max_iterations,
}
}
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
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
}
}

@ -29,6 +29,11 @@ pub struct ListenAddrConfig {
pub external: SocketAddr,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct FilteringConfig {
pub domain_blacklist: Option<PathBuf>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Config {
pub listen_addrs: Vec<ListenAddrConfig>,
@ -46,6 +51,7 @@ pub struct Config {
pub user: Option<String>,
pub group: Option<String>,
pub chroot: Option<String>,
pub filtering: FilteringConfig,
pub dnscrypt: DNSCryptConfig,
pub tls: TLSConfig,
pub daemonize: bool,

@ -372,3 +372,18 @@ pub fn serve_truncated(client_packet: Vec<u8>) -> Result<Vec<u8>, Error> {
truncate(&mut packet);
Ok(packet)
}
pub fn serve_empty_response(client_packet: Vec<u8>) -> Result<Vec<u8>, 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)
}

@ -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<BlackList>,
}

@ -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 {

@ -13,6 +13,12 @@ use tokio::prelude::*;
use tokio_net::driver::Handle;
pub async fn resolve(globals: &Globals, mut packet: &mut Vec<u8>) -> Result<Vec<u8>, 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<u8>) -> Result<Vec<
if response_addr == globals.upstream_addr
&& response_len >= 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<u8>) -> Result<Vec<
ext_socket.read_exact(&mut response).await?;
ensure!(dns::tid(&response) == tid, "Unexpected transaction ID");
ensure!(
dns::qname(&packet)? == dns::qname(&response)?,
packet_qname == dns::qname(&response)?,
"Unexpected query name in the response"
);
}

Loading…
Cancel
Save