diff --git a/Cargo.toml b/Cargo.toml index 99a234f..4f7c325 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "encrypted-dns" -version = "0.3.12" +version = "0.3.17" authors = ["Frank Denis "] edition = "2018" description = "A modern encrypted DNS server (DNSCrypt v2, Anonymized DNSCrypt, DoH)" @@ -12,20 +12,20 @@ categories = ["asynchronous", "network-programming","command-line-utilities"] readme = "README.md" [dependencies] -anyhow = "1.0" -byteorder = "1.3" -clap = { version="2.33", default-features = false, features=["wrap_help", "nightly"] } +anyhow = "1.0.31" +byteorder = "1.3.4" +clap = { version = "2.33.1", default-features = false, features = ["wrap_help"] } clockpro-cache = "0.1.8" coarsetime = "0.1.13" daemonize-simple = "0.1.4" derivative = "2.1.1" dnsstamps = "0.1.4" env_logger = { version = "0.7.1", default-features = false, features = ["humantime"] } -futures = { version = "0.3", features = ["async-await"] } -hyper = { version = "0.13", default_features = false, optional = true } -ipext = "0.1" -jemallocator = "0.3" -libsodium-sys-stable="1.19" +futures = { version = "0.3.5", features = ["async-await"] } +hyper = { version = "0.13.5", default_features = false, optional = true } +ipext = "0.1.0" +jemallocator = "0.3.2" +libsodium-sys-stable= "1.19.5" log = { version = "0.4.8", features = ["std", "release_max_level_debug"] } socket2 = "0.3" parking_lot = "0.10" diff --git a/README.md b/README.md index 6589f8e..3055802 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ All of these can be served simultaneously, on the same port (usually port 443). ### Option 1: precompiled binary for Linux Download the Encrypted DNS Server -[precompiled application for Linux (x86_64)](https://github.com/jedisct1/encrypted-dns-server/suites/433868705/artifacts/1517465). +[precompiled application for Linux (x86_64)](https://github.com/jedisct1/encrypted-dns-server/suites/612418335/artifacts/4698672) And make the application executable: @@ -32,7 +32,7 @@ chmod +x encrypted-dns Nothing else has to be installed. It doesn't require any external dependencies. -A [Debian package](https://github.com/jedisct1/encrypted-dns-server/suites/433868705/artifacts/1517464) +A [Debian package](https://github.com/jedisct1/encrypted-dns-server/suites/612418335/artifacts/4698671) for Linux/x86_64 is also available. In this package, the example configuration file can be found in `/usr/share/doc/encrypted-dns/`. @@ -109,6 +109,10 @@ Putting it in a directory that is only readable by the super-user is not a bad i Domains can be filtered directly by the proxy, see the `[filtering]` section of the configuration file. +## Access control + +Access control can be enabled in the `[access_control]` section and configured with the `query_meta` configuration value of `dnscrypt-proxy`. + ## Prometheus metrics Prometheus metrics can optionally be enabled in order to monitor performance, cache efficiency, and more. diff --git a/example-encrypted-dns.toml b/example-encrypted-dns.toml index a8226e2..d992640 100644 --- a/example-encrypted-dns.toml +++ b/example-encrypted-dns.toml @@ -83,6 +83,13 @@ cache_ttl_max = 86400 cache_ttl_error = 600 +## DNS cache: to avoid bursts of traffic for popular queries when an +## RRSET expires, hold a TTL received from an upstream server for +## `client_ttl_holdon` seconds before decreasing it in client responses. + +client_ttl_holdon = 60 + + ## Run as a background process daemonize = false @@ -114,6 +121,11 @@ daemonize = false # chroot = "/var/empty" +## Queries sent to that name will return the client IP address. +## This can be very useful for debugging, or to check that relaying works. + +my_ip = "my.ip" + #################################### # DNSCrypt settings # @@ -221,3 +233,24 @@ allow_non_reserved_ports = false # Blacklisted upstream IP addresses blacklisted_ips = [ "93.184.216.34" ] + + + + +################################ +# Access control # +################################ + +[access_control] + +# Enable access control + +enabled = false + +# Only allow access to client queries including one of these random tokens +# Tokens can be configured in the `query_meta` section of `dnscrypt-proxy` as +# `query_meta = ["token:..."]` -- Replace ... with the token to use by the client. +# Example: `query_meta = ["token:Y2oHkDJNHz"]` + +tokens = ["Y2oHkDJNHz", "G5zY3J5cHQtY", "C5zZWN1cmUuZG5z"] + diff --git a/src/blacklist.rs b/src/blacklist.rs index 5b9d99b..0c163c0 100644 --- a/src/blacklist.rs +++ b/src/blacklist.rs @@ -43,7 +43,7 @@ impl BlackList { while line.ends_with('.') { line = &line[..line.len() - 1]; } - let qname = line.as_bytes().to_vec().to_ascii_lowercase(); + let qname = line.as_bytes().to_ascii_lowercase(); if qname.is_empty() { bail!("Unexpected blacklist rule at line {}", line_nb) } @@ -53,7 +53,7 @@ impl BlackList { } pub fn find(&self, qname: &[u8]) -> bool { - let qname = qname.to_vec().to_ascii_lowercase(); + let qname = qname.to_ascii_lowercase(); let mut qname = qname.as_slice(); let map = &self.inner.map; let mut iterations = self.max_iterations; diff --git a/src/cache.rs b/src/cache.rs index 62ec5a6..d9da4b7 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -9,6 +9,7 @@ use std::sync::Arc; pub struct CachedResponse { response: Vec, expiry: Instant, + original_ttl: u32, } impl CachedResponse { @@ -16,20 +17,37 @@ impl CachedResponse { let ttl = dns::min_ttl(&response, cache.ttl_min, cache.ttl_max, cache.ttl_error) .unwrap_or(cache.ttl_error); let expiry = Instant::recent() + Duration::from_secs(u64::from(ttl)); - CachedResponse { response, expiry } + CachedResponse { + response, + expiry, + original_ttl: ttl, + } } + #[inline] pub fn set_tid(&mut self, tid: u16) { dns::set_tid(&mut self.response, tid) } + #[inline] pub fn into_response(self) -> Vec { self.response } + #[inline] pub fn has_expired(&self) -> bool { Instant::recent() > self.expiry } + + #[inline] + pub fn ttl(&self) -> u32 { + (self.expiry - Instant::recent()).as_secs() as _ + } + + #[inline] + pub fn original_ttl(&self) -> u32 { + self.original_ttl + } } #[derive(Clone, Derivative)] @@ -37,9 +55,9 @@ impl CachedResponse { pub struct Cache { #[derivative(Debug = "ignore")] cache: Arc>>, - ttl_min: u32, - ttl_max: u32, - ttl_error: u32, + pub ttl_min: u32, + pub ttl_max: u32, + pub ttl_error: u32, } impl Cache { diff --git a/src/config.rs b/src/config.rs index a59249a..192949a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,13 +2,18 @@ use crate::crypto::*; use crate::dnscrypt_certs::*; use crate::errors::*; -use std::fs::File; -use std::io::prelude::*; +use std::fs; use std::mem; use std::net::{IpAddr, SocketAddr}; use std::path::{Path, PathBuf}; use tokio::prelude::*; +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct AccessControlConfig { + pub enabled: bool, + pub tokens: Vec, +} + #[derive(Serialize, Deserialize, Debug, Clone)] pub struct AnonymizedDNSConfig { pub enabled: bool, @@ -76,9 +81,12 @@ pub struct Config { pub daemonize: bool, pub pid_file: Option, pub log_file: Option, + pub my_ip: Option, + pub client_ttl_holdon: Option, #[cfg(feature = "metrics")] pub metrics: Option, pub anonymized_dns: Option, + pub access_control: Option, } impl Config { @@ -91,9 +99,7 @@ impl Config { } pub fn from_path>(path: P) -> Result { - let mut fd = File::open(path)?; - let mut toml = String::new(); - fd.read_to_string(&mut toml)?; + let toml = fs::read_to_string(path)?; Config::from_string(&toml) } } @@ -135,9 +141,7 @@ impl State { } pub fn from_file>(path: P, key_cache_capacity: usize) -> Result { - let mut fp = File::open(path.as_ref())?; - let mut state_bin = vec![]; - fp.read_to_end(&mut state_bin)?; + let state_bin = fs::read(path)?; let mut state: State = toml::from_slice(&state_bin)?; for params_set in &mut state.dnscrypt_encryption_params_set { params_set.add_key_cache(key_cache_capacity); diff --git a/src/crypto.rs b/src/crypto.rs index 2a347cd..cbaad55 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -23,7 +23,7 @@ impl Signature { } } -big_array! { BigArray; } +big_array! { BigArray; crypto_sign_SECRETKEYBYTES as usize } #[derive(Serialize, Deserialize, Derivative, Clone)] #[derivative(Default)] diff --git a/src/dns.rs b/src/dns.rs index 2b92cd4..719817b 100644 --- a/src/dns.rs +++ b/src/dns.rs @@ -2,6 +2,7 @@ use crate::dnscrypt_certs::*; use crate::errors::*; use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; +use std::net::IpAddr; use std::sync::Arc; pub const DNS_MAX_HOSTNAME_SIZE: usize = 256; @@ -104,6 +105,12 @@ fn arcount_inc(packet: &mut [u8]) -> Result<(), Error> { Ok(()) } +#[inline] +fn arcount_clear(packet: &mut [u8]) -> Result<(), Error> { + BigEndian::write_u16(&mut packet[10..], 0); + Ok(()) +} + #[inline] pub fn an_ns_ar_count_clear(packet: &mut [u8]) { packet[6..12].iter_mut().for_each(|x| *x = 0); @@ -312,13 +319,13 @@ fn traverse_rrs Result<(), Error>>( for _ in 0..rrcount { offset = skip_name(packet, offset)?; ensure!(packet_len - offset >= 10, "Short packet"); - cb(offset)?; let rdlen = BigEndian::read_u16(&packet[offset + 8..]) as usize; - offset += 10; ensure!( - packet_len - offset >= rdlen, + packet_len - offset >= 10 + rdlen, "Record length would exceed packet length" ); + cb(offset)?; + offset += 10; offset += rdlen; } Ok(offset) @@ -334,13 +341,13 @@ fn traverse_rrs_mut Result<(), Error>>( for _ in 0..rrcount { offset = skip_name(packet, offset)?; ensure!(packet_len - offset >= 10, "Short packet"); - cb(packet, offset)?; let rdlen = BigEndian::read_u16(&packet[offset + 8..]) as usize; - offset += 10; ensure!( - packet_len - offset >= rdlen, + packet_len - offset >= 10 + rdlen, "Record length would exceed packet length" ); + cb(packet, offset)?; + offset += 10; offset += rdlen; } Ok(offset) @@ -374,6 +381,28 @@ pub fn min_ttl(packet: &[u8], min_ttl: u32, max_ttl: u32, failure_ttl: u32) -> R Ok(found_min_ttl) } +pub fn set_ttl(packet: &mut [u8], ttl: u32) -> Result<(), Error> { + let packet_len = packet.len(); + ensure!(packet_len > DNS_OFFSET_QUESTION, "Short packet"); + ensure!(packet_len <= DNS_MAX_PACKET_SIZE, "Large packet"); + ensure!(qdcount(packet) == 1, "No question"); + let mut offset = skip_name(packet, DNS_OFFSET_QUESTION)?; + assert!(offset > DNS_OFFSET_QUESTION); + ensure!(packet_len - offset > 4, "Short packet"); + offset += 4; + let (ancount, nscount, arcount) = (ancount(packet), nscount(packet), arcount(packet)); + let rrcount = ancount as usize + nscount as usize + arcount as usize; + offset = traverse_rrs_mut(packet, offset, rrcount, |packet, offset| { + let qtype = BigEndian::read_u16(&packet[offset..]); + if qtype != DNS_TYPE_OPT { + BigEndian::write_u32(&mut packet[offset + 4..], ttl) + } + Ok(()) + })?; + ensure!(packet_len == offset, "Garbage after packet"); + Ok(()) +} + fn add_edns_section(packet: &mut Vec, max_payload_size: u16) -> Result<(), Error> { let opt_rr: [u8; 11] = [ 0, @@ -502,6 +531,73 @@ pub fn qtype_qclass(packet: &[u8]) -> Result<(u16, u16), Error> { Ok((qtype, qclass)) } +fn parse_txt_rrdata Result<(), Error>>( + rrdata: &[u8], + mut cb: F, +) -> Result<(), Error> { + let rrdata_len = rrdata.len(); + let mut offset = 0; + while offset < rrdata_len { + let part_len = rrdata[offset] as usize; + if part_len == 0 { + break; + } + ensure!(rrdata_len - offset > part_len, "Short TXT RR data"); + offset += 1; + let part_bin = &rrdata[offset..offset + part_len]; + let part = std::str::from_utf8(part_bin)?; + cb(part)?; + offset += part_len; + } + Ok(()) +} + +pub fn query_meta(packet: &mut Vec) -> Result, Error> { + let packet_len = packet.len(); + ensure!(packet_len > DNS_OFFSET_QUESTION, "Short packet"); + ensure!(packet_len <= DNS_MAX_PACKET_SIZE, "Large packet"); + ensure!(qdcount(packet) == 1, "No question"); + let mut offset = skip_name(packet, DNS_OFFSET_QUESTION)?; + assert!(offset > DNS_OFFSET_QUESTION); + ensure!(packet_len - offset >= 4, "Short packet"); + offset += 4; + let (ancount, nscount, arcount) = (ancount(packet), nscount(packet), arcount(packet)); + offset = traverse_rrs( + packet, + offset, + ancount as usize + nscount as usize, + |_offset| Ok(()), + )?; + let mut token = None; + traverse_rrs(packet, offset, arcount as _, |mut offset| { + let qtype = BigEndian::read_u16(&packet[offset..]); + let qclass = BigEndian::read_u16(&packet[offset + 2..]); + if qtype != DNS_TYPE_TXT || qclass != DNS_CLASS_INET { + return Ok(()); + } + let len = BigEndian::read_u16(&packet[offset + 8..]) as usize; + offset += 10; + ensure!(packet_len - offset >= len, "Short packet"); + let rrdata = &packet[offset..offset + len]; + parse_txt_rrdata(rrdata, |txt| { + if txt.len() < 7 || !txt.starts_with("token:") { + return Ok(()); + } + ensure!(token.is_none(), "Duplicate token"); + let found_token = &txt[6..]; + let found_token = found_token.to_owned(); + token = Some(found_token); + Ok(()) + })?; + Ok(()) + })?; + if token.is_some() { + arcount_clear(packet)?; + packet.truncate(offset); + } + Ok(token) +} + pub fn serve_nxdomain_response(client_packet: Vec) -> Result, Error> { ensure!(client_packet.len() >= DNS_HEADER_SIZE, "Short packet"); ensure!(qdcount(&client_packet) == 1, "No question"); @@ -547,3 +643,37 @@ pub fn serve_blocked_response(client_packet: Vec) -> Result, Error> packet.extend_from_slice(hinfo_rdata); Ok(packet) } + +pub fn serve_ip_response(client_packet: Vec, ip: IpAddr, ttl: u32) -> 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); + an_ns_ar_count_clear(&mut packet); + authoritative_response(&mut packet); + ancount_inc(&mut packet)?; + packet.write_u16::(0xc000 + DNS_HEADER_SIZE as u16)?; + match ip { + IpAddr::V4(ip) => { + packet.write_u16::(DNS_TYPE_A)?; + packet.write_u16::(DNS_CLASS_INET)?; + packet.write_u32::(ttl)?; + packet.write_u16::(4)?; + packet.extend_from_slice(&ip.octets()); + } + IpAddr::V6(ip) => { + packet.write_u16::(DNS_TYPE_AAAA)?; + packet.write_u16::(DNS_CLASS_INET)?; + packet.write_u32::(ttl)?; + packet.write_u16::(16)?; + packet.extend_from_slice(&ip.octets()); + } + }; + Ok(packet) +} diff --git a/src/dnscrypt_certs.rs b/src/dnscrypt_certs.rs index a98f8b0..0b0aead 100644 --- a/src/dnscrypt_certs.rs +++ b/src/dnscrypt_certs.rs @@ -38,7 +38,7 @@ impl DNSCryptCertInner { } } -big_array! { BigArray; } +big_array! { BigArray; 64 } #[derive(Derivative, Serialize, Deserialize)] #[derivative(Debug, Default, Clone)] diff --git a/src/globals.rs b/src/globals.rs index 21e36ce..c855fdc 100644 --- a/src/globals.rs +++ b/src/globals.rs @@ -48,6 +48,9 @@ pub struct Globals { pub anonymized_dns_allowed_ports: Vec, pub anonymized_dns_allow_non_reserved_ports: bool, pub anonymized_dns_blacklisted_ips: Vec, + pub access_control_tokens: Option>, + pub client_ttl_holdon: u32, + pub my_ip: Option>, #[cfg(feature = "metrics")] #[derivative(Debug = "ignore")] pub varz: Varz, diff --git a/src/main.rs b/src/main.rs index 0cf0e82..8ae1740 100644 --- a/src/main.rs +++ b/src/main.rs @@ -218,7 +218,14 @@ async fn handle_client_query( !dns::is_response(&packet), "Question expected, but got a response instead" ); - let response = resolver::get_cached_response_or_resolve(&globals, &mut packet).await?; + if let Some(tokens) = &globals.access_control_tokens { + match query_meta(&mut packet)? { + None => bail!("No access token"), + Some(token) => ensure!(tokens.contains(&token), "Access token not found"), + } + } + let response = + resolver::get_cached_response_or_resolve(&globals, &client_ctx, &mut packet).await?; encrypt_and_respond_to_query( globals, client_ctx, @@ -702,6 +709,13 @@ fn main() -> Result<(), Error> { anonymized_dns.blacklisted_ips, ), }; + let access_control_tokens = match config.access_control { + Some(access_control) if access_control.enabled && !access_control.tokens.is_empty() => { + info!("Access control enabled"); + Some(access_control.tokens) + } + _ => None, + }; let runtime_handle = runtime.handle(); let globals = Arc::new(Globals { runtime_handle: runtime_handle.clone(), @@ -739,6 +753,9 @@ fn main() -> Result<(), Error> { anonymized_dns_allowed_ports, anonymized_dns_allow_non_reserved_ports, anonymized_dns_blacklisted_ips, + access_control_tokens, + my_ip: config.my_ip.map(|ip| ip.as_bytes().to_ascii_lowercase()), + client_ttl_holdon: config.client_ttl_holdon.unwrap_or(60), #[cfg(feature = "metrics")] varz: Varz::default(), }); diff --git a/src/resolver.rs b/src/resolver.rs index 790e85e..5ebb944 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -2,10 +2,12 @@ use crate::cache::*; use crate::dns::{self, *}; use crate::errors::*; use crate::globals::*; +use crate::ClientCtx; use byteorder::{BigEndian, ByteOrder}; use rand::prelude::*; use siphasher::sip128::Hasher128; +use std::cmp; use std::hash::Hasher; use std::net::{Ipv6Addr, SocketAddr, SocketAddrV6}; use tokio::net::{TcpStream, UdpSocket}; @@ -226,9 +228,20 @@ pub async fn resolve( pub async fn get_cached_response_or_resolve( globals: &Globals, + client_ctx: &ClientCtx, mut packet: &mut Vec, ) -> Result, Error> { let packet_qname = dns::qname(&packet)?; + if let Some(my_ip) = &globals.my_ip { + if &packet_qname.to_ascii_lowercase() == my_ip { + let client_ip = match client_ctx { + ClientCtx::Udp(u) => u.client_addr, + ClientCtx::Tcp(t) => t.client_connection.peer_addr()?, + } + .ip(); + return serve_ip_response(packet.to_vec(), client_ip, 1); + } + } if let Some(blacklist) = &globals.blacklist { if blacklist.find(&packet_qname) { #[cfg(feature = "metrics")] @@ -276,7 +289,14 @@ pub async fn get_cached_response_or_resolve( #[cfg(feature = "metrics")] globals.varz.client_queries_cached.inc(); cached_response.set_tid(original_tid); + let original_ttl = cached_response.original_ttl(); + let mut ttl = cached_response.ttl(); + if ttl.saturating_add(globals.client_ttl_holdon) > original_ttl { + ttl = original_ttl; + } + ttl = cmp::max(1, ttl); let mut response = cached_response.into_response(); + dns::set_ttl(&mut response, ttl)?; dns::recase_qname(&mut response, &packet_qname)?; return Ok(response); }