Fix DNS domain name case preservation and DNS 0x20 encoding issues.
Fixed DNS 0x20 encoding bug in worker threads and removed automatic lowercasing in DNS buffer parsing to preserve case from authoritative sources. Implemented case-insensitive lookups for cache and blockchain while ensuring restoration of the original client query case in all response paths instead of returning randomized DNS 0x20 case from upstream servers.
This commit is contained in:
Generated
+1
-1
@@ -45,7 +45,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "alfis"
|
name = "alfis"
|
||||||
version = "0.8.7"
|
version = "0.8.8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bincode",
|
"bincode",
|
||||||
"blakeout",
|
"blakeout",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "alfis"
|
name = "alfis"
|
||||||
version = "0.8.7"
|
version = "0.8.8"
|
||||||
authors = ["Revertron <alfis@revertron.com>"]
|
authors = ["Revertron <alfis@revertron.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|||||||
@@ -230,9 +230,11 @@ impl BlockchainFilter {
|
|||||||
|
|
||||||
impl DnsFilter for BlockchainFilter {
|
impl DnsFilter for BlockchainFilter {
|
||||||
fn lookup(&self, qname: &str, qtype: QueryType, recursive: bool) -> Option<DnsPacket> {
|
fn lookup(&self, qname: &str, qtype: QueryType, recursive: bool) -> Option<DnsPacket> {
|
||||||
|
// Lowercase for case-insensitive lookup (blockchain stores domains as lowercase)
|
||||||
|
let qname_lower = qname.to_lowercase();
|
||||||
let top_domain;
|
let top_domain;
|
||||||
let subdomain;
|
let subdomain;
|
||||||
let parts: Vec<&str> = qname.rsplitn(3, '.').collect();
|
let parts: Vec<&str> = qname_lower.rsplitn(3, '.').collect();
|
||||||
match parts.len() {
|
match parts.len() {
|
||||||
1 => {
|
1 => {
|
||||||
let mut packet = DnsPacket::new();
|
let mut packet = DnsPacket::new();
|
||||||
|
|||||||
+1
-1
@@ -136,7 +136,7 @@ pub trait PacketBuffer {
|
|||||||
outstr.push_str(delim);
|
outstr.push_str(delim);
|
||||||
|
|
||||||
let str_buffer = self.get_range(pos, len as usize)?;
|
let str_buffer = self.get_range(pos, len as usize)?;
|
||||||
outstr.push_str(&String::from_utf8_lossy(str_buffer).to_lowercase());
|
outstr.push_str(&String::from_utf8_lossy(str_buffer));
|
||||||
|
|
||||||
delim = ".";
|
delim = ".";
|
||||||
|
|
||||||
|
|||||||
+22
-11
@@ -132,7 +132,7 @@ impl DomainEntry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fill_queryresult(&self, qtype: QueryType, result_vec: &mut Vec<DnsRecord>) {
|
pub fn fill_queryresult(&self, qname: &str, qtype: QueryType, result_vec: &mut Vec<DnsRecord>) {
|
||||||
let now = Local::now();
|
let now = Local::now();
|
||||||
|
|
||||||
let current_set = match self.record_types.get(&qtype) {
|
let current_set = match self.record_types.get(&qtype) {
|
||||||
@@ -149,7 +149,10 @@ impl DomainEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if entry.record.get_querytype() == qtype {
|
if entry.record.get_querytype() == qtype {
|
||||||
result_vec.push(entry.record.clone());
|
let mut record = entry.record.clone();
|
||||||
|
// Preserve the original query case in the response
|
||||||
|
record.set_domain(qname.to_string());
|
||||||
|
result_vec.push(record);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,17 +177,21 @@ impl Cache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn fill_queryresult(&mut self, qname: &str, qtype: QueryType, result_vec: &mut Vec<DnsRecord>, increment_stats: bool) {
|
fn fill_queryresult(&mut self, qname: &str, qtype: QueryType, result_vec: &mut Vec<DnsRecord>, increment_stats: bool) {
|
||||||
if let Some(domain_entry) = self.domain_entries.get_mut(qname).and_then(Arc::get_mut) {
|
// DNS is case-insensitive, so lowercase for cache lookup
|
||||||
|
let qname_lower = qname.to_lowercase();
|
||||||
|
if let Some(domain_entry) = self.domain_entries.get_mut(&qname_lower).and_then(Arc::get_mut) {
|
||||||
if increment_stats {
|
if increment_stats {
|
||||||
domain_entry.hits += 1
|
domain_entry.hits += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
domain_entry.fill_queryresult(qtype, result_vec);
|
domain_entry.fill_queryresult(qname, qtype, result_vec);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn lookup(&mut self, qname: &str, qtype: QueryType) -> Option<DnsPacket> {
|
pub fn lookup(&mut self, qname: &str, qtype: QueryType) -> Option<DnsPacket> {
|
||||||
match self.get_cache_state(qname, qtype) {
|
// DNS is case-insensitive, so lowercase for cache lookup
|
||||||
|
let qname_lower = qname.to_lowercase();
|
||||||
|
match self.get_cache_state(&qname_lower, qtype) {
|
||||||
CacheState::PositiveCache => {
|
CacheState::PositiveCache => {
|
||||||
let mut qr = DnsPacket::new();
|
let mut qr = DnsPacket::new();
|
||||||
self.fill_queryresult(qname, qtype, &mut qr.answers, true);
|
self.fill_queryresult(qname, qtype, &mut qr.answers, true);
|
||||||
@@ -208,27 +215,31 @@ impl Cache {
|
|||||||
Some(x) => x,
|
Some(x) => x,
|
||||||
None => continue
|
None => continue
|
||||||
};
|
};
|
||||||
|
// Store with a lowercase key for case-insensitive lookups
|
||||||
|
let domain_lower = domain.to_lowercase();
|
||||||
|
|
||||||
if let Some(ref mut rs) = self.domain_entries.get_mut(&domain).and_then(Arc::get_mut) {
|
if let Some(ref mut rs) = self.domain_entries.get_mut(&domain_lower).and_then(Arc::get_mut) {
|
||||||
rs.store_record(rec);
|
rs.store_record(rec);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut rs = DomainEntry::new(domain.clone());
|
let mut rs = DomainEntry::new(domain_lower.clone());
|
||||||
rs.store_record(rec);
|
rs.store_record(rec);
|
||||||
self.domain_entries.insert(domain.clone(), Arc::new(rs));
|
self.domain_entries.insert(domain_lower, Arc::new(rs));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn store_nxdomain(&mut self, qname: &str, qtype: QueryType, ttl: u32) {
|
pub fn store_nxdomain(&mut self, qname: &str, qtype: QueryType, ttl: u32) {
|
||||||
if let Some(ref mut rs) = self.domain_entries.get_mut(qname).and_then(Arc::get_mut) {
|
// Store with lowercase key for case-insensitive lookups
|
||||||
|
let qname_lower = qname.to_lowercase();
|
||||||
|
if let Some(ref mut rs) = self.domain_entries.get_mut(&qname_lower).and_then(Arc::get_mut) {
|
||||||
rs.store_nxdomain(qtype, ttl);
|
rs.store_nxdomain(qtype, ttl);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut rs = DomainEntry::new(qname.to_string());
|
let mut rs = DomainEntry::new(qname_lower.clone());
|
||||||
rs.store_nxdomain(qtype, ttl);
|
rs.store_nxdomain(qtype, ttl);
|
||||||
self.domain_entries.insert(qname.to_string(), Arc::new(rs));
|
self.domain_entries.insert(qname_lower, Arc::new(rs));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-4
@@ -271,6 +271,8 @@ impl DnsClient for DnsNetworkClient {
|
|||||||
let pending_queries_lock = self.pending_queries.clone();
|
let pending_queries_lock = self.pending_queries.clone();
|
||||||
let stopped = Arc::clone(&self.stopped);
|
let stopped = Arc::clone(&self.stopped);
|
||||||
|
|
||||||
|
let match_case = self.enable_0x20;
|
||||||
|
|
||||||
Builder::new()
|
Builder::new()
|
||||||
.name("DnsNetworkClient-worker-thread-v4".into())
|
.name("DnsNetworkClient-worker-thread-v4".into())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
@@ -308,7 +310,8 @@ impl DnsClient for DnsNetworkClient {
|
|||||||
// Validate 0x20 encoding - response must match query case exactly
|
// Validate 0x20 encoding - response must match query case exactly
|
||||||
if !packet.questions.is_empty() {
|
if !packet.questions.is_empty() {
|
||||||
let response_name = &packet.questions[0].name;
|
let response_name = &packet.questions[0].name;
|
||||||
if response_name != &pending_query.query_name {
|
if (match_case && response_name != &pending_query.query_name)
|
||||||
|
|| (pending_query.query_name.to_lowercase() != response_name.to_lowercase()) {
|
||||||
trace!("Rejecting response with mismatched case: expected '{}', got '{}'",
|
trace!("Rejecting response with mismatched case: expected '{}', got '{}'",
|
||||||
pending_query.query_name, response_name);
|
pending_query.query_name, response_name);
|
||||||
continue;
|
continue;
|
||||||
@@ -342,6 +345,8 @@ impl DnsClient for DnsNetworkClient {
|
|||||||
let pending_queries_lock = self.pending_queries.clone();
|
let pending_queries_lock = self.pending_queries.clone();
|
||||||
let stopped = Arc::clone(&self.stopped);
|
let stopped = Arc::clone(&self.stopped);
|
||||||
|
|
||||||
|
let match_case = self.enable_0x20;
|
||||||
|
|
||||||
Builder::new()
|
Builder::new()
|
||||||
.name("DnsNetworkClient-worker-thread-v6".into())
|
.name("DnsNetworkClient-worker-thread-v6".into())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
@@ -379,7 +384,8 @@ impl DnsClient for DnsNetworkClient {
|
|||||||
// Validate 0x20 encoding - response must match query case exactly
|
// Validate 0x20 encoding - response must match query case exactly
|
||||||
if !packet.questions.is_empty() {
|
if !packet.questions.is_empty() {
|
||||||
let response_name = &packet.questions[0].name;
|
let response_name = &packet.questions[0].name;
|
||||||
if response_name != &pending_query.query_name {
|
if (match_case && response_name != &pending_query.query_name)
|
||||||
|
|| (pending_query.query_name.to_lowercase() != response_name.to_lowercase()) {
|
||||||
trace!("Rejecting response with mismatched case: expected '{}', got '{}'",
|
trace!("Rejecting response with mismatched case: expected '{}', got '{}'",
|
||||||
pending_query.query_name, response_name);
|
pending_query.query_name, response_name);
|
||||||
continue;
|
continue;
|
||||||
@@ -461,7 +467,7 @@ impl DnsClient for DnsNetworkClient {
|
|||||||
pub struct HttpsDnsClient {
|
pub struct HttpsDnsClient {
|
||||||
agent: Agent,
|
agent: Agent,
|
||||||
/// Counter for assigning packet ids
|
/// Counter for assigning packet ids
|
||||||
seq: AtomicUsize,
|
seq: AtomicU16,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "doh")]
|
#[cfg(feature = "doh")]
|
||||||
@@ -481,7 +487,7 @@ impl HttpsDnsClient {
|
|||||||
.max_idle_connections(16)
|
.max_idle_connections(16)
|
||||||
.build();
|
.build();
|
||||||
let agent = Agent::with_parts(agent_config, DefaultConnector::default(), BootstrapResolver::new(servers));
|
let agent = Agent::with_parts(agent_config, DefaultConnector::default(), BootstrapResolver::new(servers));
|
||||||
Self { agent, seq: AtomicUsize::new(1) }
|
Self { agent, seq: AtomicU16::new(rand::random::<u16>()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -505,6 +505,23 @@ impl DnsRecord {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_domain(&mut self, new_domain: String) {
|
||||||
|
match self {
|
||||||
|
DnsRecord::A { ref mut domain, .. }
|
||||||
|
| DnsRecord::AAAA { ref mut domain, .. }
|
||||||
|
| DnsRecord::NS { ref mut domain, .. }
|
||||||
|
| DnsRecord::CNAME { ref mut domain, .. }
|
||||||
|
| DnsRecord::SRV { ref mut domain, .. }
|
||||||
|
| DnsRecord::PTR { ref mut domain, .. }
|
||||||
|
| DnsRecord::MX { ref mut domain, .. }
|
||||||
|
| DnsRecord::UNKNOWN { ref mut domain, .. }
|
||||||
|
| DnsRecord::SOA { ref mut domain, .. }
|
||||||
|
| DnsRecord::TXT { ref mut domain, .. }
|
||||||
|
| DnsRecord::TLSA { ref mut domain, .. } => *domain = new_domain,
|
||||||
|
DnsRecord::OPT { .. } => {} // OPT records don't have a domain field
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_data(&self) -> Option<String> {
|
pub fn get_data(&self) -> Option<String> {
|
||||||
match *self {
|
match *self {
|
||||||
DnsRecord::A { ref addr, .. } => Some(addr.to_string()),
|
DnsRecord::A { ref addr, .. } => Some(addr.to_string()),
|
||||||
|
|||||||
+48
-4
@@ -87,7 +87,7 @@ impl DnsResolver for ForwardingDnsResolver {
|
|||||||
fn perform(&mut self, qname: &str, qtype: QueryType) -> Result<DnsPacket> {
|
fn perform(&mut self, qname: &str, qtype: QueryType) -> Result<DnsPacket> {
|
||||||
let mut random = rand::thread_rng();
|
let mut random = rand::thread_rng();
|
||||||
let upstream = self.upstreams.iter().choose(&mut random).unwrap();
|
let upstream = self.upstreams.iter().choose(&mut random).unwrap();
|
||||||
let result = match self.context.cache.lookup(qname, qtype) {
|
let mut result = match self.context.cache.lookup(qname, qtype) {
|
||||||
None => {
|
None => {
|
||||||
if is_url(upstream) {
|
if is_url(upstream) {
|
||||||
if let Some(client) = &self.context.doh_client {
|
if let Some(client) = &self.context.doh_client {
|
||||||
@@ -105,6 +105,17 @@ impl DnsResolver for ForwardingDnsResolver {
|
|||||||
|
|
||||||
self.context.cache.store(&result.answers)?;
|
self.context.cache.store(&result.answers)?;
|
||||||
|
|
||||||
|
// Fix domain names in answers to match original query case (DNS 0x20 may have randomized them)
|
||||||
|
let qname_lower = qname.to_lowercase();
|
||||||
|
for answer in &mut result.answers {
|
||||||
|
if let Some(domain) = answer.get_domain() {
|
||||||
|
// Only fix if it matches the query (case-insensitive)
|
||||||
|
if domain.to_lowercase() == qname_lower {
|
||||||
|
answer.set_domain(qname.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,7 +180,18 @@ impl DnsResolver for RecursiveDnsResolver {
|
|||||||
let _ = self.context.cache.store(&response.answers);
|
let _ = self.context.cache.store(&response.answers);
|
||||||
let _ = self.context.cache.store(&response.authorities);
|
let _ = self.context.cache.store(&response.authorities);
|
||||||
let _ = self.context.cache.store(&response.resources);
|
let _ = self.context.cache.store(&response.resources);
|
||||||
return Ok(response);
|
|
||||||
|
// Fix domain names in answers to match original query case
|
||||||
|
let qname_lower = qname.to_lowercase();
|
||||||
|
let mut fixed_response = response;
|
||||||
|
for answer in &mut fixed_response.answers {
|
||||||
|
if let Some(domain) = answer.get_domain() {
|
||||||
|
if domain.to_lowercase() == qname_lower {
|
||||||
|
answer.set_domain(qname.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Ok(fixed_response);
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.header.rescode == ResultCode::NXDOMAIN {
|
if response.header.rescode == ResultCode::NXDOMAIN {
|
||||||
@@ -194,7 +216,19 @@ impl DnsResolver for RecursiveDnsResolver {
|
|||||||
// If not, we'll have to resolve the ip of a NS record
|
// If not, we'll have to resolve the ip of a NS record
|
||||||
let new_ns_name = match response.get_unresolved_ns(qname) {
|
let new_ns_name = match response.get_unresolved_ns(qname) {
|
||||||
Some(x) => x,
|
Some(x) => x,
|
||||||
None => return Ok(response)
|
None => {
|
||||||
|
// Fix domain names before returning
|
||||||
|
let qname_lower = qname.to_lowercase();
|
||||||
|
let mut fixed_response = response;
|
||||||
|
for answer in &mut fixed_response.answers {
|
||||||
|
if let Some(domain) = answer.get_domain() {
|
||||||
|
if domain.to_lowercase() == qname_lower {
|
||||||
|
answer.set_domain(qname.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Ok(fixed_response);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Recursively resolve the NS
|
// Recursively resolve the NS
|
||||||
@@ -204,7 +238,17 @@ impl DnsResolver for RecursiveDnsResolver {
|
|||||||
if let Some(new_ns) = recursive_response.get_random_a() {
|
if let Some(new_ns) = recursive_response.get_random_a() {
|
||||||
ns = new_ns.clone();
|
ns = new_ns.clone();
|
||||||
} else {
|
} else {
|
||||||
return Ok(response);
|
// Fix domain names before returning
|
||||||
|
let qname_lower = qname.to_lowercase();
|
||||||
|
let mut fixed_response = response;
|
||||||
|
for answer in &mut fixed_response.answers {
|
||||||
|
if let Some(domain) = answer.get_domain() {
|
||||||
|
if domain.to_lowercase() == qname_lower {
|
||||||
|
answer.set_domain(qname.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Ok(fixed_response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-5
@@ -103,8 +103,8 @@ pub struct Dns {
|
|||||||
impl Default for Dns {
|
impl Default for Dns {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Dns {
|
Dns {
|
||||||
listen: String::from("127.0.0.1:53"),
|
listen: default_listen_dns(),
|
||||||
threads: 20,
|
threads: 10,
|
||||||
forwarders: vec![String::from("94.140.14.14:53"), String::from("94.140.15.15:53")],
|
forwarders: vec![String::from("94.140.14.14:53"), String::from("94.140.15.15:53")],
|
||||||
bootstraps: default_dns_bootstraps(),
|
bootstraps: default_dns_bootstraps(),
|
||||||
hosts: Vec::new(),
|
hosts: Vec::new(),
|
||||||
@@ -136,7 +136,7 @@ pub struct Net {
|
|||||||
impl Default for Net {
|
impl Default for Net {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Net {
|
Net {
|
||||||
peers: vec![String::from("test-ip4.alfis.name:4244"), String::from("test-ip6.alfis.name:4244")],
|
peers: vec![String::from("peer-v4.alfis.name:4244"), String::from("peer-v6.alfis.name:4244")],
|
||||||
listen: String::from("[::]:4244"),
|
listen: String::from("[::]:4244"),
|
||||||
public: true,
|
public: true,
|
||||||
yggdrasil_only: false
|
yggdrasil_only: false
|
||||||
@@ -149,11 +149,11 @@ fn default_listen() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn default_listen_dns() -> String {
|
fn default_listen_dns() -> String {
|
||||||
String::from("0.0.0.0:53")
|
String::from("127.0.0.3:53")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_threads() -> usize {
|
fn default_threads() -> usize {
|
||||||
100
|
10
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_check_blocks() -> u64 {
|
fn default_check_blocks() -> u64 {
|
||||||
|
|||||||
Reference in New Issue
Block a user