From c9767db8799227585bac12d297f36074fddb338c Mon Sep 17 00:00:00 2001 From: Xander Bil Date: Thu, 26 Sep 2024 00:00:51 +0200 Subject: [PATCH] Respond with fixed SOA of zones --- zns-daemon/src/config.rs | 9 +- zns-daemon/src/db/models.rs | 13 +-- zns-daemon/src/handlers/query.rs | 65 ++++++++++++-- .../src/handlers/update/authenticate.rs | 10 ++- zns-daemon/src/handlers/update/mod.rs | 8 +- zns-daemon/src/handlers/update/sig.rs | 5 +- zns/src/labelstring.rs | 85 +++++++++++++++++++ zns/src/lib.rs | 2 +- zns/src/message.rs | 20 ++--- zns/src/parser.rs | 30 +++++-- zns/src/structs.rs | 12 ++- zns/src/test_utils.rs | 11 +-- zns/src/utils.rs | 34 -------- 13 files changed, 209 insertions(+), 95 deletions(-) create mode 100644 zns/src/labelstring.rs delete mode 100644 zns/src/utils.rs diff --git a/zns-daemon/src/config.rs b/zns-daemon/src/config.rs index f2fa788..84360c5 100644 --- a/zns-daemon/src/config.rs +++ b/zns-daemon/src/config.rs @@ -1,13 +1,14 @@ use std::{env, net::IpAddr, sync::OnceLock}; use dotenvy::dotenv; +use zns::labelstring::LabelString; static CONFIG: OnceLock = OnceLock::new(); pub struct Config { pub zauth_url: String, pub db_uri: String, - pub authoritative_zone: Vec, + pub authoritative_zone: LabelString, pub port: u16, pub address: IpAddr, } @@ -25,11 +26,7 @@ impl Config { Config { db_uri: env::var("DATABASE_URL").expect("DATABASE_URL must be set"), zauth_url: env::var("ZAUTH_URL").expect("ZAUTH_URL must be set"), - authoritative_zone: env::var("ZONE") - .expect("ZONE must be set") - .split('.') - .map(str::to_string) - .collect(), + authoritative_zone: LabelString::from(&env::var("ZONE").expect("ZONE must be set")), port: env::var("ZNS_PORT") .map(|v| v.parse::().expect("ZNS_PORT is invalid")) .unwrap_or(5333), diff --git a/zns-daemon/src/db/models.rs b/zns-daemon/src/db/models.rs index 1a11677..7d59cde 100644 --- a/zns-daemon/src/db/models.rs +++ b/zns-daemon/src/db/models.rs @@ -2,6 +2,7 @@ use diesel::prelude::*; use diesel::sql_types::Text; use zns::{ errors::ZNSError, + labelstring::LabelString, structs::{Class, Type, RR}, }; @@ -103,7 +104,7 @@ pub fn insert_into_database(rr: &RR, connection: &mut PgConnection) -> Result<() } let record = Record { - name: rr.name.join("."), + name: rr.name.to_string(), _type: rr._type.clone().into(), class: rr.class.clone().into(), ttl: rr.ttl, @@ -119,14 +120,14 @@ pub fn insert_into_database(rr: &RR, connection: &mut PgConnection) -> Result<() } pub fn get_from_database( - name: &[String], + name: &LabelString, _type: Option, class: Class, connection: &mut PgConnection, ) -> Result, ZNSError> { let records = Record::get( connection, - name.join("."), + name.to_string(), _type.map(|t| t.into()), class.into(), ) @@ -137,7 +138,7 @@ pub fn get_from_database( Ok(records .into_iter() .map(|record| RR { - name: record.name.split('.').map(str::to_string).collect(), + name: LabelString::from(&record.name), _type: Type::from(record._type as u16), class: Class::from(record.class as u16), ttl: record.ttl, @@ -149,7 +150,7 @@ pub fn get_from_database( //TODO: cleanup models pub fn delete_from_database( - name: &[String], + name: &LabelString, _type: Option, class: Class, rdata: Option>, @@ -157,7 +158,7 @@ pub fn delete_from_database( ) { let _ = Record::delete( connection, - name.join("."), + name.to_string(), _type.map(|f| f.into()), class.into(), rdata, diff --git a/zns-daemon/src/handlers/query.rs b/zns-daemon/src/handlers/query.rs index b109a67..2a71bfd 100644 --- a/zns-daemon/src/handlers/query.rs +++ b/zns-daemon/src/handlers/query.rs @@ -2,7 +2,9 @@ use diesel::PgConnection; use zns::{ errors::ZNSError, - structs::{Message, Question, RR}, + labelstring::LabelString, + parser::ToBytes, + structs::{Class, Message, Question, RRClass, RRType, SoaRData, Type, RR}, }; use crate::{config::Config, db::models::get_from_database}; @@ -35,10 +37,14 @@ impl ResponseHandler for QueryHandler { if rrs.is_empty() { rrs.extend(try_wildcard(question, connection)?); if rrs.is_empty() { - return Err(ZNSError::NXDomain { - domain: question.qname.join("."), - qtype: question.qtype.clone(), - }); + if question.qtype == Type::Type(RRType::SOA) { + rrs.extend([get_soa(&question.qname)?]) + } else { + return Err(ZNSError::NXDomain { + domain: question.qname.to_string(), + qtype: question.qtype.clone(), + }); + } } } response.header.ancount += rrs.len() as u16; @@ -59,13 +65,13 @@ impl ResponseHandler for QueryHandler { fn try_wildcard(question: &Question, connection: &mut PgConnection) -> Result, ZNSError> { let records = get_from_database(&question.qname, None, question.qclass.clone(), connection)?; - if !records.is_empty() || question.qname.is_empty() { + if !records.is_empty() || question.qname.as_slice().is_empty() { Ok(vec![]) } else { - let mut qname = question.qname.clone(); - qname[0] = String::from("*"); + let qname = question.qname.clone().to_vec(); + qname.to_vec()[0] = String::from("*"); Ok(get_from_database( - &qname, + &qname.into(), Some(question.qtype.clone()), question.qclass.clone(), connection, @@ -79,6 +85,47 @@ fn try_wildcard(question: &Question, connection: &mut PgConnection) -> Result Result { + let auth_zone = Config::get().authoritative_zone.clone(); + let rdata = if &Config::get().authoritative_zone == name { + // Recommended values taken from wikipedia: https://en.wikipedia.org/wiki/SOA_record + Ok(SoaRData { + mname: auth_zone, + rname: LabelString::from("admin.zeus.ugent.be"), + serial: 1, + refresh: 86400, + retry: 7200, + expire: 3600000, + minimum: 172800, + }) + } else if name.len() > auth_zone.len() { + let zone: LabelString = name.as_slice()[name.len() - auth_zone.len() - 1..].into(); + Ok(SoaRData { + mname: zone.clone(), + rname: LabelString::from(&format!("{}.zeus.ugent.be", zone.as_slice()[0])), + serial: 1, + refresh: 86400, + retry: 7200, + expire: 3600000, + minimum: 172800, + }) + } else { + Err(ZNSError::NXDomain { + domain: name.to_string(), + qtype: Type::Type(RRType::SOA), + }) + }?; + + Ok(RR { + name: name.to_owned(), + _type: Type::Type(RRType::SOA), + class: Class::Class(RRClass::IN), + ttl: 11200, + rdlength: 0, + rdata: SoaRData::to_bytes(rdata), + }) +} + #[cfg(test)] mod tests { use super::*; diff --git a/zns-daemon/src/handlers/update/authenticate.rs b/zns-daemon/src/handlers/update/authenticate.rs index e362231..b2a2369 100644 --- a/zns-daemon/src/handlers/update/authenticate.rs +++ b/zns-daemon/src/handlers/update/authenticate.rs @@ -5,9 +5,10 @@ use crate::{config::Config, db::models::get_from_database}; use zns::{ errors::ZNSError, + labelstring::LabelString, parser::FromBytes, reader::Reader, - structs::{Class, LabelString, RRClass, RRType, Type}, + structs::{Class, RRClass, RRType, Type}, }; use super::{dnskey::DNSKeyRData, sig::Sig}; @@ -17,8 +18,9 @@ pub async fn authenticate( zone: &LabelString, connection: &mut PgConnection, ) -> Result { - if zone.len() > Config::get().authoritative_zone.len() { - let username = &zone[zone.len() - Config::get().authoritative_zone.len() - 1]; + if zone.as_slice().len() > Config::get().authoritative_zone.as_slice().len() { + let username = &zone.as_slice() + [zone.as_slice().len() - Config::get().authoritative_zone.as_slice().len() - 1]; let ssh_verified = validate_ssh(&username.to_lowercase(), sig) .await @@ -62,7 +64,7 @@ async fn validate_ssh(username: &String, sig: &Sig) -> Result Result { diff --git a/zns-daemon/src/handlers/update/mod.rs b/zns-daemon/src/handlers/update/mod.rs index addefcd..b7a6cb1 100644 --- a/zns-daemon/src/handlers/update/mod.rs +++ b/zns-daemon/src/handlers/update/mod.rs @@ -5,8 +5,8 @@ use crate::{ db::models::{delete_from_database, insert_into_database}, }; +use zns::errors::ZNSError; use zns::structs::{Class, Message, RRClass, RRType, Type}; -use zns::{errors::ZNSError, utils::labels_equal}; use self::sig::Sig; @@ -41,7 +41,7 @@ impl ResponseHandler for UpdateHandler { // Check Prerequisite TODO: implement this let zone = &message.question[0]; - let zlen = zone.qname.len(); + let zlen = zone.qname.as_slice().len(); //TODO: this code is ugly let last = message.additional.last(); @@ -61,10 +61,10 @@ impl ResponseHandler for UpdateHandler { // Update Section Prescan for rr in &message.authority { - let rlen = rr.name.len(); + let rlen = rr.name.as_slice().len(); // Check if rr has same zone - if rlen < zlen || !(labels_equal(&zone.qname, &rr.name[rlen - zlen..].into())) { + if rlen < zlen || !(&zone.qname == &rr.name.as_slice()[rlen - zlen..].into()) { return Err(ZNSError::Refused { message: "RR has different zone from Question".to_string(), }); diff --git a/zns-daemon/src/handlers/update/sig.rs b/zns-daemon/src/handlers/update/sig.rs index ac3526d..97534cd 100644 --- a/zns-daemon/src/handlers/update/sig.rs +++ b/zns-daemon/src/handlers/update/sig.rs @@ -4,10 +4,7 @@ use base64::prelude::*; use int_enum::IntEnum; use zns::{ - errors::ZNSError, - parser::FromBytes, - reader::Reader, - structs::{LabelString, RR}, + errors::ZNSError, labelstring::LabelString, parser::FromBytes, reader::Reader, structs::RR, }; use super::{ diff --git a/zns/src/labelstring.rs b/zns/src/labelstring.rs new file mode 100644 index 0000000..c9bd981 --- /dev/null +++ b/zns/src/labelstring.rs @@ -0,0 +1,85 @@ +use std::fmt::Display; + + +#[derive(Debug, Clone)] +pub struct LabelString(Vec); + +pub fn labels_equal(vec1: &LabelString, vec2: &LabelString) -> bool { + if vec1.as_slice().len() != vec2.as_slice().len() { + return false; + } + + for (elem1, elem2) in vec1.as_slice().iter().zip(vec2.as_slice().iter()) { + if elem1.to_lowercase() != elem2.to_lowercase() { + return false; + } + } + + true +} + +impl LabelString { + pub fn from(string: &str) -> Self { + LabelString(string.split('.').map(str::to_string).collect()) + } + + pub fn as_slice(&self) -> &[String] { + self.0.as_slice() + } + + pub fn to_vec(self) -> Vec { + self.0 + } + + pub fn len(&self) -> usize { + self.0.len() + } + + #[must_use] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +impl PartialEq for LabelString { + fn eq(&self, other: &Self) -> bool { + labels_equal(self, other) + } +} + +impl From<&[String]> for LabelString { + fn from(value: &[String]) -> Self { + LabelString(value.to_vec()) + } +} + +impl From> for LabelString { + fn from(value: Vec) -> Self { + LabelString(value) + } +} + +impl Display for LabelString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.join(".")) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_labels_equal() { + assert!(labels_equal( + &LabelString::from("one.two"), + &LabelString::from("oNE.two") + )); + + assert!(!labels_equal( + &LabelString::from("onne.two"), + &LabelString::from("oNEe.two") + )); + } +} diff --git a/zns/src/lib.rs b/zns/src/lib.rs index a7b7558..e5f8b40 100644 --- a/zns/src/lib.rs +++ b/zns/src/lib.rs @@ -1,8 +1,8 @@ pub mod errors; +pub mod labelstring; pub mod message; pub mod parser; pub mod reader; pub mod structs; pub mod test_utils; -pub mod utils; diff --git a/zns/src/message.rs b/zns/src/message.rs index f12de1d..d72c4bf 100644 --- a/zns/src/message.rs +++ b/zns/src/message.rs @@ -1,7 +1,7 @@ use crate::{ errors::ZNSError, - structs::{LabelString, Message, Opcode, RCODE}, - utils::labels_equal, + labelstring::LabelString, + structs::{Message, Opcode, RCODE}, }; impl Message { @@ -23,10 +23,12 @@ impl Message { for question in &self.question { let zlen = question.qname.len(); if !(zlen >= auth_zone.len() - && labels_equal(&question.qname[zlen - auth_zone.len()..].into(), auth_zone)) + && &Into::::into( + question.qname.as_slice()[zlen - auth_zone.len()..].to_vec(), + ) == auth_zone) { return Err(ZNSError::Refused { - message: format!("Not authoritative for: {}", question.qname.join(".")), + message: format!("Not authoritative for: {}", question.qname), }); } } @@ -69,20 +71,16 @@ mod tests { #[test] fn test_authoritative() { - let name = vec![ - String::from("not"), - String::from("good"), - String::from("zone"), - ]; + let name = LabelString::from("not.good.zone"); let message = get_message(Some(name)); assert!(message - .check_authoritative(&vec![String::from("good")]) + .check_authoritative(&LabelString::from("good")) .is_err_and(|x| x.rcode() == RCODE::REFUSED)); assert!(message - .check_authoritative(&vec![String::from("Zone")]) + .check_authoritative(&LabelString::from("Zone")) .is_ok()) } } diff --git a/zns/src/parser.rs b/zns/src/parser.rs index 17aecff..041cdb1 100644 --- a/zns/src/parser.rs +++ b/zns/src/parser.rs @@ -2,8 +2,9 @@ use std::mem::size_of; use crate::{ errors::ZNSError, + labelstring::LabelString, reader::Reader, - structs::{Class, Header, LabelString, Message, Opcode, Question, RRClass, RRType, Type, RR}, + structs::{Class, Header, Message, Opcode, Question, RRClass, RRType, SoaRData, Type, RR}, }; type Result = std::result::Result; @@ -143,17 +144,17 @@ impl FromBytes for LabelString { if code & 0b11000000 != 0 { let offset = (((code & 0b00111111) as u16) << 8) | reader.read_u8()? as u16; let mut reader_past = reader.seek(offset as usize)?; - out.extend(LabelString::from_bytes(&mut reader_past)?); + out.extend(LabelString::from_bytes(&mut reader_past)?.to_vec()); } - Ok(out) + Ok(out.into()) } } impl ToBytes for LabelString { fn to_bytes(name: Self) -> Vec { let mut result: Vec = vec![]; - for label in name { + for label in name.as_slice() { result.push(label.len() as u8); result.extend(label.as_bytes()); } @@ -289,6 +290,19 @@ impl ToBytes for Message { } } +impl ToBytes for SoaRData { + fn to_bytes(rdata: Self) -> Vec { + let mut result = LabelString::to_bytes(rdata.mname); + result.extend(LabelString::to_bytes(rdata.rname)); + result.extend(u32::to_be_bytes(rdata.serial)); + result.extend(i32::to_be_bytes(rdata.refresh)); + result.extend(i32::to_be_bytes(rdata.retry)); + result.extend(i32::to_be_bytes(rdata.expire)); + result.extend(u32::to_be_bytes(rdata.minimum)); + result + } +} + #[cfg(test)] pub mod tests { use crate::test_utils::{get_message, get_rr}; @@ -315,7 +329,7 @@ pub mod tests { #[test] fn test_parse_question() { let question = Question { - qname: vec![String::from("example"), String::from("org")], + qname: LabelString::from("example.org"), qtype: Type::Type(RRType::A), qclass: Class::Class(RRClass::IN), }; @@ -338,7 +352,7 @@ pub mod tests { #[test] fn test_labelstring() { - let labelstring = vec![String::from("example"), String::from("org")]; + let labelstring: LabelString = vec![String::from("example"), String::from("org")].into(); let bytes = LabelString::to_bytes(labelstring.clone()); let parsed = LabelString::from_bytes(&mut Reader::new(&bytes)); @@ -348,7 +362,7 @@ pub mod tests { #[test] fn test_labelstring_ptr() { - let labelstring = vec![String::from("example"), String::from("org")]; + let labelstring: LabelString = vec![String::from("example"), String::from("org")].into(); let mut bytes = LabelString::to_bytes(labelstring.clone()); @@ -370,7 +384,7 @@ pub mod tests { #[test] fn test_labelstring_invalid_ptr() { - let labelstring = vec![String::from("example"), String::from("org")]; + let labelstring: LabelString = vec![String::from("example"), String::from("org")].into(); let mut bytes = LabelString::to_bytes(labelstring.clone()); diff --git a/zns/src/structs.rs b/zns/src/structs.rs index 7b4edbb..008405c 100644 --- a/zns/src/structs.rs +++ b/zns/src/structs.rs @@ -1,5 +1,7 @@ use int_enum::IntEnum; +use crate::labelstring::LabelString; + #[derive(Debug, Clone, PartialEq)] pub enum Type { Type(RRType), @@ -89,4 +91,12 @@ pub struct RR { pub rdata: Vec, } -pub type LabelString = Vec; +pub struct SoaRData { + pub mname: LabelString, + pub rname: LabelString, + pub serial: u32, + pub refresh: i32, + pub retry: i32, + pub expire: i32, + pub minimum: u32, +} diff --git a/zns/src/test_utils.rs b/zns/src/test_utils.rs index dac00d6..cfe730e 100644 --- a/zns/src/test_utils.rs +++ b/zns/src/test_utils.rs @@ -2,9 +2,10 @@ use crate::structs::*; #[cfg(feature = "test-utils")] +use crate::labelstring::LabelString; pub fn get_rr(name: Option) -> RR { RR { - name: name.unwrap_or(vec![String::from("example"), String::from("org")]), + name: name.unwrap_or(LabelString::from("example.org")), _type: Type::Type(RRType::A), class: Class::Class(RRClass::IN), ttl: 10, @@ -25,16 +26,12 @@ pub fn get_message(name: Option) -> Message { }, question: vec![ Question { - qname: name - .clone() - .unwrap_or(vec![String::from("example"), String::from("org")]), + qname: name.clone().unwrap_or(LabelString::from("example.org")), qtype: Type::Type(RRType::A), qclass: Class::Class(RRClass::IN), }, Question { - qname: name - .clone() - .unwrap_or(vec![String::from("example"), String::from("org")]), + qname: name.clone().unwrap_or(LabelString::from("example.org")), qtype: Type::Type(RRType::A), qclass: Class::Class(RRClass::IN), }, diff --git a/zns/src/utils.rs b/zns/src/utils.rs deleted file mode 100644 index f3c98ef..0000000 --- a/zns/src/utils.rs +++ /dev/null @@ -1,34 +0,0 @@ -use crate::structs::LabelString; - -pub fn labels_equal(vec1: &LabelString, vec2: &LabelString) -> bool { - if vec1.len() != vec2.len() { - return false; - } - - for (elem1, elem2) in vec1.iter().zip(vec2.iter()) { - if elem1.to_lowercase() != elem2.to_lowercase() { - return false; - } - } - - true -} - -#[cfg(test)] -mod tests { - - use super::*; - - #[test] - fn test_labels_equal() { - assert!(labels_equal( - &vec![String::from("one"), String::from("two")], - &vec![String::from("oNE"), String::from("two")] - )); - - assert!(!labels_equal( - &vec![String::from("one"), String::from("two")], - &vec![String::from("oNEe"), String::from("two")] - )); - } -}