From 76dc8065838acfbe752d818d4555315a4b4d6429 Mon Sep 17 00:00:00 2001 From: notplants Date: Tue, 4 May 2021 16:49:16 +0200 Subject: [PATCH 01/18] borrowed-after-move --- src/dns.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/dns.rs b/src/dns.rs index 1ef2512..549d3a1 100644 --- a/src/dns.rs +++ b/src/dns.rs @@ -81,6 +81,18 @@ pub async fn server() -> ServerFuture { Box::new(Arc::new(RwLock::new(authority))), ); + // second insert + let dyn_name = Name::from_str("test.dyn.peach.cloud.").unwrap(); + let dyn_ttl = 60; + let dyn_rdata = RData::A(Ipv4Addr::new(1, 1, 1, 3)); + let dyn_record = Record::from_rdata(dyn_name, dyn_ttl, dyn_rdata); + authority.upsert(dyn_record, authority.serial()); + + catalog.upsert( + LowerName::new(&authority_name), + Box::new(Arc::new(RwLock::new(authority))), + ); + let mut server = ServerFuture::new(catalog); // load all the listeners From 8bd7e7b1b40a5822412bb8e0b34ef6963388d538 Mon Sep 17 00:00:00 2001 From: notplants Date: Fri, 7 May 2021 12:20:22 +0200 Subject: [PATCH 02/18] Working on multiple insert --- src/dns.rs | 178 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 103 insertions(+), 75 deletions(-) diff --git a/src/dns.rs b/src/dns.rs index 549d3a1..cb63542 100644 --- a/src/dns.rs +++ b/src/dns.rs @@ -13,87 +13,115 @@ use trust_dns_server::store::in_memory::InMemoryAuthority; static DEFAULT_TCP_REQUEST_TIMEOUT: u64 = 5; + +struct DnsManager { + catalog: Catalog, +} + +impl DnsManager { + pub fn new() -> DnsManager { + + let catalog: Catalog = Catalog::new(); + + return DnsManager { + catalog, + }; + } + + pub fn upsert(&mut self, domain: String, ip: Ipv4Addr) { + + + let authority_name = Name::from_str("dyn.peach.cloud.").unwrap(); + + let soa_serial = 1; + let soa_name = Name::from_str("dyn.peach.cloud.").unwrap(); + let soa_rdata = RData::SOA(SOA::new( + Name::from_str("dyn.peach.cloud.").unwrap(), // mname + Name::from_str("root.dyn.peach.cloud.").unwrap(), // rname + soa_serial, // serial + 604800, // refresh + 86400, // retry + 2419200, // expire + 86400, // negtive cache ttl + )); + let mut soa_record_set = RecordSet::new(&soa_name, RecordType::SOA, soa_serial); + soa_record_set.add_rdata(soa_rdata); + let soa_rr_key = RrKey::new( + LowerName::new(&authority_name), + soa_record_set.record_type(), + ); + let mut authority_records = BTreeMap::new(); + authority_records.insert(soa_rr_key, soa_record_set); + + let authority_zone_type = ZoneType::Master; + let authority_allow_axfr = false; + + let mut authority = InMemoryAuthority::new( + authority_name.clone(), + authority_records, + authority_zone_type, + authority_allow_axfr, + ) + .unwrap(); + + /* + let ns_name = Name::from_str("dyn.peach.cloud.").unwrap(); + let ns_ttl = 60; + let ns_rdata = RData::NS(Name::from_str("localhost.").unwrap()); + let ns_record = Record::from_rdata(ns_name, ns_ttl, ns_rdata); + authority.upsert(ns_record, authority.serial()); + */ + + let dyn_name = Name::from_str(&domain).unwrap(); + let dyn_ttl = 60; + let dyn_rdata = RData::A(ip); + let dyn_record = Record::from_rdata(dyn_name, dyn_ttl, dyn_rdata); + authority.upsert(dyn_record, authority.serial()); + + self.catalog.upsert( + LowerName::new(&authority_name), + Box::new(Arc::new(RwLock::new(authority))), + ); + } +} + + pub async fn server() -> ServerFuture { info!("Trust-DNS {} starting", trust_dns_server::version()); + let mut dns_manager = DnsManager::new(); + + // first insert + dns_manager.upsert( + "test.dyn.peachcloud.org".to_string(), + Ipv4Addr::new(1, 1, 1, 1), + ); + +// // second insert +// dns_manager.upsert( +// "test.dyn.peachcloud.org.".to_string(), +// Ipv4Addr::new(1, 1, 1, 3), +// ); +// +// // third insert +// dns_manager.upsert( +// "peach.dyn.peachcloud.org.".to_string(), +// Ipv4Addr::new(1, 1, 2, 3), +// ); + let ip_addr = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)); - let listen_port: u16 = 12323; - let tcp_request_timeout = Duration::from_secs(DEFAULT_TCP_REQUEST_TIMEOUT); + let listen_port: u16 = 12323; + let tcp_request_timeout = Duration::from_secs(DEFAULT_TCP_REQUEST_TIMEOUT); - let sock_addr = SocketAddr::new(ip_addr, listen_port); - let udp_socket = UdpSocket::bind(&sock_addr) - .await - .expect("could not bind udp socket"); - let tcp_listener = TcpListener::bind(&sock_addr) - .await - .expect("could not bind tcp listener"); + let sock_addr = SocketAddr::new(ip_addr, listen_port); + let udp_socket = UdpSocket::bind(&sock_addr) + .await + .expect("could not bind udp socket"); + let tcp_listener = TcpListener::bind(&sock_addr) + .await + .expect("could not bind tcp listener"); - let mut catalog: Catalog = Catalog::new(); - - let authority_name = Name::from_str("dyn.peach.cloud.").unwrap(); - let mut authority_records = BTreeMap::new(); - let authority_zone_type = ZoneType::Master; - let authority_allow_axfr = false; - - let soa_serial = 1; - let soa_name = Name::from_str("dyn.peach.cloud.").unwrap(); - let soa_rdata = RData::SOA(SOA::new( - Name::from_str("dyn.peach.cloud.").unwrap(), // mname - Name::from_str("root.dyn.peach.cloud.").unwrap(), // rname - soa_serial, // serial - 604800, // refresh - 86400, // retry - 2419200, // expire - 86400, // negtive cache ttl - )); - let mut soa_record_set = RecordSet::new(&soa_name, RecordType::SOA, soa_serial); - soa_record_set.add_rdata(soa_rdata); - let soa_rr_key = RrKey::new( - LowerName::new(&authority_name), - soa_record_set.record_type(), - ); - authority_records.insert(soa_rr_key, soa_record_set); - - let mut authority = InMemoryAuthority::new( - authority_name.clone(), - authority_records, - authority_zone_type, - authority_allow_axfr, - ) - .unwrap(); - - /* - let ns_name = Name::from_str("dyn.peach.cloud.").unwrap(); - let ns_ttl = 60; - let ns_rdata = RData::NS(Name::from_str("localhost.").unwrap()); - let ns_record = Record::from_rdata(ns_name, ns_ttl, ns_rdata); - authority.upsert(ns_record, authority.serial()); - */ - - let dyn_name = Name::from_str("test.dyn.peach.cloud.").unwrap(); - let dyn_ttl = 60; - let dyn_rdata = RData::A(Ipv4Addr::new(1, 1, 1, 1)); - let dyn_record = Record::from_rdata(dyn_name, dyn_ttl, dyn_rdata); - authority.upsert(dyn_record, authority.serial()); - - catalog.upsert( - LowerName::new(&authority_name), - Box::new(Arc::new(RwLock::new(authority))), - ); - - // second insert - let dyn_name = Name::from_str("test.dyn.peach.cloud.").unwrap(); - let dyn_ttl = 60; - let dyn_rdata = RData::A(Ipv4Addr::new(1, 1, 1, 3)); - let dyn_record = Record::from_rdata(dyn_name, dyn_ttl, dyn_rdata); - authority.upsert(dyn_record, authority.serial()); - - catalog.upsert( - LowerName::new(&authority_name), - Box::new(Arc::new(RwLock::new(authority))), - ); - - let mut server = ServerFuture::new(catalog); + let mut server = ServerFuture::new(dns_manager.catalog); // load all the listeners info!("DNS server listening for UDP on {:?}", udp_socket); From c6d531660736050837a67572fcda544bf5e88bf2 Mon Sep 17 00:00:00 2001 From: notplants Date: Mon, 10 May 2021 09:47:32 +0200 Subject: [PATCH 03/18] Using dyn.peachcloud.org as the authority --- src/dns.rs | 138 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 124 insertions(+), 14 deletions(-) diff --git a/src/dns.rs b/src/dns.rs index cb63542..0e0fab0 100644 --- a/src/dns.rs +++ b/src/dns.rs @@ -31,13 +31,13 @@ impl DnsManager { pub fn upsert(&mut self, domain: String, ip: Ipv4Addr) { - let authority_name = Name::from_str("dyn.peach.cloud.").unwrap(); + let authority_name = Name::from_str("dyn.peachcloud.org.").unwrap(); let soa_serial = 1; - let soa_name = Name::from_str("dyn.peach.cloud.").unwrap(); + let soa_name = Name::from_str("dyn.peachcloud.org.").unwrap(); let soa_rdata = RData::SOA(SOA::new( - Name::from_str("dyn.peach.cloud.").unwrap(), // mname - Name::from_str("root.dyn.peach.cloud.").unwrap(), // rname + Name::from_str("dyn.peachcloud.org.").unwrap(), // mname + Name::from_str("root.dyn.peachcloud.org.").unwrap(), // rname soa_serial, // serial 604800, // refresh 86400, // retry @@ -65,7 +65,7 @@ impl DnsManager { .unwrap(); /* - let ns_name = Name::from_str("dyn.peach.cloud.").unwrap(); + let ns_name = Name::from_str("dyn.peachcloud.org.").unwrap(); let ns_ttl = 60; let ns_rdata = RData::NS(Name::from_str("localhost.").unwrap()); let ns_record = Record::from_rdata(ns_name, ns_ttl, ns_rdata); @@ -83,6 +83,114 @@ impl DnsManager { Box::new(Arc::new(RwLock::new(authority))), ); } + + fn get_initial_records(&mut self) -> BTreeMap { + let authority_name = Name::from_str("dyn.peachcloud.org.").unwrap(); + let soa_serial = 1; + let soa_name = Name::from_str("dyn.peachcloud.org.").unwrap(); + let soa_rdata = RData::SOA(SOA::new( + Name::from_str("dyn.peachcloud.org.").unwrap(), // mname + Name::from_str("root.dyn.peachcloud.org.").unwrap(), // rname + soa_serial, // serial + 604800, // refresh + 86400, // retry + 2419200, // expire + 86400, // negtive cache ttl + )); + let mut soa_record_set = RecordSet::new(&soa_name, RecordType::SOA, soa_serial); + soa_record_set.add_rdata(soa_rdata); + let soa_rr_key = RrKey::new( + LowerName::new(&authority_name), + soa_record_set.record_type(), + ); + let mut authority_records = BTreeMap::new(); + authority_records.insert(soa_rr_key, soa_record_set); + authority_records + } + + fn upsert_test(&mut self) { + + let authority_records = self.get_initial_records(); + + let authority_name = Name::from_str("dyn.peachcloud.org.").unwrap(); + + let authority_zone_type = ZoneType::Master; + let authority_allow_axfr = false; + + // first upsert + let domain1 = "test.dyn.peachcloud.org"; + let ip1 = Ipv4Addr::new(1, 1, 1, 1); + + let mut authority = InMemoryAuthority::new( + authority_name.clone(), + authority_records, + authority_zone_type, + authority_allow_axfr, + ) + .unwrap(); + + let dyn_name = Name::from_str(domain1).unwrap(); + let dyn_ttl = 60; + let dyn_rdata = RData::A(ip1); + let dyn_record = Record::from_rdata(dyn_name, dyn_ttl, dyn_rdata); + authority.upsert(dyn_record, authority.serial()); + + let authority_ref = &authority; + + self.catalog.upsert( + LowerName::new(&authority_name), + Box::new(Arc::new(RwLock::new(authority))), + ); + + // test second insert + let domain2 = "who.dyn.peachcloud.org"; + let ip2 = Ipv4Addr::new(1, 1, 1, 2); + let dyn_name = Name::from_str(domain2).unwrap(); + let dyn_ttl = 60; + let dyn_rdata = RData::A(ip2); + let dyn_record = Record::from_rdata(dyn_name, dyn_ttl, dyn_rdata); + authority.upsert(dyn_record, authority.serial()); + + // test if it worked + let dyn_name = Name::from_str(domain1).unwrap(); + let lower_name = LowerName::from(dyn_name); + let found = self.catalog.contains(&lower_name); + println!("++ found {:?}: {:?}", lower_name.to_string(), found); + + let dyn_name = Name::from_str(domain2).unwrap(); + let lower_name = LowerName::from(dyn_name); + let found = self.catalog.contains(&lower_name); + println!("++ found {:?}: {:?}", lower_name.to_string(), found); + + let lower_name = LowerName::new(&authority_name); + let found = self.catalog.contains(&lower_name); + println!("++ found {:?}: {:?}", lower_name.to_string(), found); + + +// // second upsert +// let domain2 = "peach.dyn.peachcloud.org"; +// let ip2 = Ipv4Addr::new(1, 1, 1, 2); +// let authority_records = self.get_initial_records(); +// +// let mut authority = InMemoryAuthority::new( +// authority_name.clone(), +// authority_records, +// authority_zone_type, +// authority_allow_axfr, +// ) +// .unwrap(); +// +// let dyn_name = Name::from_str(domain2).unwrap(); +// let dyn_ttl = 60; +// let dyn_rdata = RData::A(ip2); +// let dyn_record = Record::from_rdata(dyn_name, dyn_ttl, dyn_rdata); +// authority.upsert(dyn_record, authority.serial()); +// +// self.catalog.upsert( +// LowerName::new(&authority_name), +// Box::new(Arc::new(RwLock::new(authority))), +// ); + } } @@ -91,24 +199,26 @@ pub async fn server() -> ServerFuture { let mut dns_manager = DnsManager::new(); - // first insert - dns_manager.upsert( - "test.dyn.peachcloud.org".to_string(), - Ipv4Addr::new(1, 1, 1, 1), - ); - +// // first insert +// dns_manager.upsert( +// "test.dyn.peachcloud.org".to_string(), +// Ipv4Addr::new(1, 1, 1, 1), +// ); +// // // second insert // dns_manager.upsert( -// "test.dyn.peachcloud.org.".to_string(), +// "test.dyn.peachcloud.org".to_string(), // Ipv4Addr::new(1, 1, 1, 3), // ); // // // third insert // dns_manager.upsert( -// "peach.dyn.peachcloud.org.".to_string(), -// Ipv4Addr::new(1, 1, 2, 3), +// "peach.dyn.peachcloud.org".to_string(), +// Ipv4Addr::new(1, 1 , 2, 3), // ); + dns_manager.upsert_test(); + let ip_addr = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)); let listen_port: u16 = 12323; let tcp_request_timeout = Duration::from_secs(DEFAULT_TCP_REQUEST_TIMEOUT); From 1b563ccbea771830afdcef2943794febd1a44f1b Mon Sep 17 00:00:00 2001 From: notplants Date: Mon, 10 May 2021 10:46:15 +0200 Subject: [PATCH 04/18] One zone per subdomain --- .env.sample | 1 + Cargo.lock | 7 ++ Cargo.toml | 1 + src/dns.rs | 238 ++++++++++++++++++++++++++-------------------------- 4 files changed, 127 insertions(+), 120 deletions(-) create mode 100644 .env.sample diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..85b60ca --- /dev/null +++ b/.env.sample @@ -0,0 +1 @@ +DYN_ROOT_ZONE= \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 2d5f9e1..8f8c8ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -331,6 +331,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "dtoa" version = "0.4.8" @@ -1142,6 +1148,7 @@ version = "0.1.0" dependencies = [ "clap-log-flag", "clap-verbosity-flag", + "dotenv", "futures 0.3.14", "log", "nest", diff --git a/Cargo.toml b/Cargo.toml index 26fc678..c69e78f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,3 +20,4 @@ trust-dns-client = "0.20.2" rocket = { git = "https://github.com/SergioBenitez/Rocket", branch = "master" } rocket_contrib = { git = "https://github.com/SergioBenitez/Rocket", branch = "master" } serde = "1.0.125" +dotenv = "0.15.0" \ No newline at end of file diff --git a/src/dns.rs b/src/dns.rs index 0e0fab0..8986a13 100644 --- a/src/dns.rs +++ b/src/dns.rs @@ -10,87 +10,91 @@ use trust_dns_client::rr::{LowerName, Name, RData, Record, RecordSet, RecordType use trust_dns_server::authority::{Catalog, ZoneType}; use trust_dns_server::server::ServerFuture; use trust_dns_server::store::in_memory::InMemoryAuthority; +use std::env; +use dotenv; static DEFAULT_TCP_REQUEST_TIMEOUT: u64 = 5; struct DnsManager { catalog: Catalog, + dyn_root_zone: String, } impl DnsManager { - pub fn new() -> DnsManager { + pub fn new(dyn_root_zone: String) -> DnsManager { let catalog: Catalog = Catalog::new(); return DnsManager { catalog, + dyn_root_zone, }; } +// +// pub fn upsert(&mut self, domain: String, ip: Ipv4Addr) { +// +// +// let authority_name = Name::from_str(&self.dyn_root_zone).unwrap(); +// +// let soa_serial = 1; +// let soa_name = Name::from_str(&self.dyn_root_zone).unwrap(); +// let soa_rdata = RData::SOA(SOA::new( +// Name::from_str(&self.dyn_root_zone).unwrap(), // mname +// Name::from_str(&format!("root.{}", &self.dyn_root_zone)).unwrap(), // rname +// soa_serial, // serial +// 604800, // refresh +// 86400, // retry +// 2419200, // expire +// 86400, // negtive cache ttl +// )); +// let mut soa_record_set = RecordSet::new(&soa_name, RecordType::SOA, soa_serial); +// soa_record_set.add_rdata(soa_rdata); +// let soa_rr_key = RrKey::new( +// LowerName::new(&authority_name), +// soa_record_set.record_type(), +// ); +// let mut authority_records = BTreeMap::new(); +// authority_records.insert(soa_rr_key, soa_record_set); +// +// let authority_zone_type = ZoneType::Master; +// let authority_allow_axfr = false; +// +// let mut authority = InMemoryAuthority::new( +// authority_name.clone(), +// authority_records, +// authority_zone_type, +// authority_allow_axfr, +// ) +// .unwrap(); +// +// /* +// let ns_name = Name::from_str("dyn.peachcloud.org.").unwrap(); +// let ns_ttl = 60; +// let ns_rdata = RData::NS(Name::from_str("localhost.").unwrap()); +// let ns_record = Record::from_rdata(ns_name, ns_ttl, ns_rdata); +// authority.upsert(ns_record, authority.serial()); +// */ +// +// let dyn_name = Name::from_str(&domain).unwrap(); +// let dyn_ttl = 60; +// let dyn_rdata = RData::A(ip); +// let dyn_record = Record::from_rdata(dyn_name, dyn_ttl, dyn_rdata); +// authority.upsert(dyn_record, authority.serial()); +// +// self.catalog.upsert( +// LowerName::new(&authority_name), +// Box::new(Arc::new(RwLock::new(authority))), +// ); +// } - pub fn upsert(&mut self, domain: String, ip: Ipv4Addr) { - - - let authority_name = Name::from_str("dyn.peachcloud.org.").unwrap(); - + fn get_initial_records(domain: &str) -> BTreeMap { + let authority_name = Name::from_str(domain).unwrap(); let soa_serial = 1; - let soa_name = Name::from_str("dyn.peachcloud.org.").unwrap(); + let soa_name = Name::from_str(domain).unwrap(); let soa_rdata = RData::SOA(SOA::new( - Name::from_str("dyn.peachcloud.org.").unwrap(), // mname - Name::from_str("root.dyn.peachcloud.org.").unwrap(), // rname - soa_serial, // serial - 604800, // refresh - 86400, // retry - 2419200, // expire - 86400, // negtive cache ttl - )); - let mut soa_record_set = RecordSet::new(&soa_name, RecordType::SOA, soa_serial); - soa_record_set.add_rdata(soa_rdata); - let soa_rr_key = RrKey::new( - LowerName::new(&authority_name), - soa_record_set.record_type(), - ); - let mut authority_records = BTreeMap::new(); - authority_records.insert(soa_rr_key, soa_record_set); - - let authority_zone_type = ZoneType::Master; - let authority_allow_axfr = false; - - let mut authority = InMemoryAuthority::new( - authority_name.clone(), - authority_records, - authority_zone_type, - authority_allow_axfr, - ) - .unwrap(); - - /* - let ns_name = Name::from_str("dyn.peachcloud.org.").unwrap(); - let ns_ttl = 60; - let ns_rdata = RData::NS(Name::from_str("localhost.").unwrap()); - let ns_record = Record::from_rdata(ns_name, ns_ttl, ns_rdata); - authority.upsert(ns_record, authority.serial()); - */ - - let dyn_name = Name::from_str(&domain).unwrap(); - let dyn_ttl = 60; - let dyn_rdata = RData::A(ip); - let dyn_record = Record::from_rdata(dyn_name, dyn_ttl, dyn_rdata); - authority.upsert(dyn_record, authority.serial()); - - self.catalog.upsert( - LowerName::new(&authority_name), - Box::new(Arc::new(RwLock::new(authority))), - ); - } - - fn get_initial_records(&mut self) -> BTreeMap { - let authority_name = Name::from_str("dyn.peachcloud.org.").unwrap(); - let soa_serial = 1; - let soa_name = Name::from_str("dyn.peachcloud.org.").unwrap(); - let soa_rdata = RData::SOA(SOA::new( - Name::from_str("dyn.peachcloud.org.").unwrap(), // mname - Name::from_str("root.dyn.peachcloud.org.").unwrap(), // rname + Name::from_str(domain).unwrap(), // mname + Name::from_str(&format!("root.{}", domain)).unwrap(), // rname soa_serial, // serial 604800, // refresh 86400, // retry @@ -108,19 +112,14 @@ impl DnsManager { authority_records } - fn upsert_test(&mut self) { + pub fn upsert(&mut self, domain: &str, ip: Ipv4Addr) { - let authority_records = self.get_initial_records(); - - let authority_name = Name::from_str("dyn.peachcloud.org.").unwrap(); + let authority_records = DnsManager::get_initial_records(domain); + let authority_name = Name::from_str(domain).unwrap(); let authority_zone_type = ZoneType::Master; let authority_allow_axfr = false; - // first upsert - let domain1 = "test.dyn.peachcloud.org"; - let ip1 = Ipv4Addr::new(1, 1, 1, 1); - let mut authority = InMemoryAuthority::new( authority_name.clone(), authority_records, @@ -129,67 +128,60 @@ impl DnsManager { ) .unwrap(); - let dyn_name = Name::from_str(domain1).unwrap(); + let dyn_name = Name::from_str(domain).unwrap(); let dyn_ttl = 60; - let dyn_rdata = RData::A(ip1); + let dyn_rdata = RData::A(ip); let dyn_record = Record::from_rdata(dyn_name, dyn_ttl, dyn_rdata); authority.upsert(dyn_record, authority.serial()); - let authority_ref = &authority; - self.catalog.upsert( LowerName::new(&authority_name), Box::new(Arc::new(RwLock::new(authority))), ); + } - // test second insert - let domain2 = "who.dyn.peachcloud.org"; + fn root_upsert(&mut self) { + let authority_records = DnsManager::get_initial_records(&self.dyn_root_zone); + let authority_name = Name::from_str(&self.dyn_root_zone).unwrap(); + + let authority_zone_type = ZoneType::Master; + let authority_allow_axfr = false; + + // first upsert, for root + let authority = InMemoryAuthority::new( + authority_name.clone(), + authority_records, + authority_zone_type, + authority_allow_axfr, + ) + .unwrap(); + self.catalog.upsert( + LowerName::new(&authority_name), + Box::new(Arc::new(RwLock::new(authority))), + ); + } + + + fn upsert_test(&mut self) { + + // first insert the authority for the root dyn zone + self.root_upsert(); + + // second upsert, for sub-sub + let domain1 = &format!("test.{}", self.dyn_root_zone); + let ip1 = Ipv4Addr::new(1, 1, 1, 1); + self.upsert(domain1, ip1); + + // third upsert, for sub-sub + let domain2 = &format!("peach.{}", self.dyn_root_zone); let ip2 = Ipv4Addr::new(1, 1, 1, 2); - let dyn_name = Name::from_str(domain2).unwrap(); - let dyn_ttl = 60; - let dyn_rdata = RData::A(ip2); - let dyn_record = Record::from_rdata(dyn_name, dyn_ttl, dyn_rdata); - authority.upsert(dyn_record, authority.serial()); + self.upsert(domain2, ip2); - // test if it worked - let dyn_name = Name::from_str(domain1).unwrap(); - let lower_name = LowerName::from(dyn_name); - let found = self.catalog.contains(&lower_name); - println!("++ found {:?}: {:?}", lower_name.to_string(), found); + // update upsert, for sub-sub + let domain2 = &format!("test.{}", self.dyn_root_zone); + let ip2 = Ipv4Addr::new(1, 1, 1, 3); + self.upsert(domain2, ip2); - let dyn_name = Name::from_str(domain2).unwrap(); - let lower_name = LowerName::from(dyn_name); - let found = self.catalog.contains(&lower_name); - println!("++ found {:?}: {:?}", lower_name.to_string(), found); - - let lower_name = LowerName::new(&authority_name); - let found = self.catalog.contains(&lower_name); - println!("++ found {:?}: {:?}", lower_name.to_string(), found); - - -// // second upsert -// let domain2 = "peach.dyn.peachcloud.org"; -// let ip2 = Ipv4Addr::new(1, 1, 1, 2); -// let authority_records = self.get_initial_records(); -// -// let mut authority = InMemoryAuthority::new( -// authority_name.clone(), -// authority_records, -// authority_zone_type, -// authority_allow_axfr, -// ) -// .unwrap(); -// -// let dyn_name = Name::from_str(domain2).unwrap(); -// let dyn_ttl = 60; -// let dyn_rdata = RData::A(ip2); -// let dyn_record = Record::from_rdata(dyn_name, dyn_ttl, dyn_rdata); -// authority.upsert(dyn_record, authority.serial()); -// -// self.catalog.upsert( -// LowerName::new(&authority_name), -// Box::new(Arc::new(RwLock::new(authority))), -// ); } } @@ -197,7 +189,9 @@ impl DnsManager { pub async fn server() -> ServerFuture { info!("Trust-DNS {} starting", trust_dns_server::version()); - let mut dns_manager = DnsManager::new(); + dotenv::from_path("/etc/peach-dyndns.conf").ok(); + let dyn_root_zone = env::var("DYN_ROOT_ZONE").expect("DYN_ROOT_ZONE not set"); + let mut dns_manager = DnsManager::new(dyn_root_zone.to_string()); // // first insert // dns_manager.upsert( @@ -241,5 +235,9 @@ pub async fn server() -> ServerFuture { server.register_listener(tcp_listener, tcp_request_timeout); info!("awaiting DNS connections..."); + let domain3 = &format!("question.{}", dns_manager.dyn_root_zone); + let ip3 = Ipv4Addr::new(1, 1, 1, 5); + dns_manager.upsert(domain3, ip3); + server } From f45632f0b7df90ffb048ee8d663e1b09f8964954 Mon Sep 17 00:00:00 2001 From: notplants Date: Mon, 10 May 2021 11:14:59 +0200 Subject: [PATCH 05/18] working on single authority --- src/dns.rs | 116 +++++++++-------------------------------------------- 1 file changed, 20 insertions(+), 96 deletions(-) diff --git a/src/dns.rs b/src/dns.rs index 8986a13..6290746 100644 --- a/src/dns.rs +++ b/src/dns.rs @@ -31,62 +31,6 @@ impl DnsManager { dyn_root_zone, }; } -// -// pub fn upsert(&mut self, domain: String, ip: Ipv4Addr) { -// -// -// let authority_name = Name::from_str(&self.dyn_root_zone).unwrap(); -// -// let soa_serial = 1; -// let soa_name = Name::from_str(&self.dyn_root_zone).unwrap(); -// let soa_rdata = RData::SOA(SOA::new( -// Name::from_str(&self.dyn_root_zone).unwrap(), // mname -// Name::from_str(&format!("root.{}", &self.dyn_root_zone)).unwrap(), // rname -// soa_serial, // serial -// 604800, // refresh -// 86400, // retry -// 2419200, // expire -// 86400, // negtive cache ttl -// )); -// let mut soa_record_set = RecordSet::new(&soa_name, RecordType::SOA, soa_serial); -// soa_record_set.add_rdata(soa_rdata); -// let soa_rr_key = RrKey::new( -// LowerName::new(&authority_name), -// soa_record_set.record_type(), -// ); -// let mut authority_records = BTreeMap::new(); -// authority_records.insert(soa_rr_key, soa_record_set); -// -// let authority_zone_type = ZoneType::Master; -// let authority_allow_axfr = false; -// -// let mut authority = InMemoryAuthority::new( -// authority_name.clone(), -// authority_records, -// authority_zone_type, -// authority_allow_axfr, -// ) -// .unwrap(); -// -// /* -// let ns_name = Name::from_str("dyn.peachcloud.org.").unwrap(); -// let ns_ttl = 60; -// let ns_rdata = RData::NS(Name::from_str("localhost.").unwrap()); -// let ns_record = Record::from_rdata(ns_name, ns_ttl, ns_rdata); -// authority.upsert(ns_record, authority.serial()); -// */ -// -// let dyn_name = Name::from_str(&domain).unwrap(); -// let dyn_ttl = 60; -// let dyn_rdata = RData::A(ip); -// let dyn_record = Record::from_rdata(dyn_name, dyn_ttl, dyn_rdata); -// authority.upsert(dyn_record, authority.serial()); -// -// self.catalog.upsert( -// LowerName::new(&authority_name), -// Box::new(Arc::new(RwLock::new(authority))), -// ); -// } fn get_initial_records(domain: &str) -> BTreeMap { let authority_name = Name::from_str(domain).unwrap(); @@ -112,42 +56,22 @@ impl DnsManager { authority_records } - pub fn upsert(&mut self, domain: &str, ip: Ipv4Addr) { - - let authority_records = DnsManager::get_initial_records(domain); - let authority_name = Name::from_str(domain).unwrap(); - - let authority_zone_type = ZoneType::Master; - let authority_allow_axfr = false; - - let mut authority = InMemoryAuthority::new( - authority_name.clone(), - authority_records, - authority_zone_type, - authority_allow_axfr, - ) - .unwrap(); - - let dyn_name = Name::from_str(domain).unwrap(); + fn upsert_domain(mut authority: InMemoryAuthority, domain: String, ip: Ipv4Addr) { + let dyn_name = Name::from_str(&domain).unwrap(); let dyn_ttl = 60; let dyn_rdata = RData::A(ip); let dyn_record = Record::from_rdata(dyn_name, dyn_ttl, dyn_rdata); authority.upsert(dyn_record, authority.serial()); - - self.catalog.upsert( - LowerName::new(&authority_name), - Box::new(Arc::new(RwLock::new(authority))), - ); } - fn root_upsert(&mut self) { + fn build_catalog(&mut self) { let authority_records = DnsManager::get_initial_records(&self.dyn_root_zone); let authority_name = Name::from_str(&self.dyn_root_zone).unwrap(); let authority_zone_type = ZoneType::Master; let authority_allow_axfr = false; - // first upsert, for root + // first create an authority for root_dyn_zone let authority = InMemoryAuthority::new( authority_name.clone(), authority_records, @@ -155,6 +79,13 @@ impl DnsManager { authority_allow_axfr, ) .unwrap(); + + // then upsert records into the authority for all records in database + let domain1 = format!("test.{}", self.dyn_root_zone); + let ip1 = Ipv4Addr::new(1, 1, 1, 1); + DnsManager::upsert_domain(authority, domain1, ip1); + + // finally put the authority into the catalog self.catalog.upsert( LowerName::new(&authority_name), Box::new(Arc::new(RwLock::new(authority))), @@ -165,22 +96,19 @@ impl DnsManager { fn upsert_test(&mut self) { // first insert the authority for the root dyn zone - self.root_upsert(); + self.build_catalog(); // second upsert, for sub-sub - let domain1 = &format!("test.{}", self.dyn_root_zone); - let ip1 = Ipv4Addr::new(1, 1, 1, 1); - self.upsert(domain1, ip1); // third upsert, for sub-sub - let domain2 = &format!("peach.{}", self.dyn_root_zone); - let ip2 = Ipv4Addr::new(1, 1, 1, 2); - self.upsert(domain2, ip2); - - // update upsert, for sub-sub - let domain2 = &format!("test.{}", self.dyn_root_zone); - let ip2 = Ipv4Addr::new(1, 1, 1, 3); - self.upsert(domain2, ip2); +// let domain2 = &format!("peach.{}", self.dyn_root_zone); +// let ip2 = Ipv4Addr::new(1, 1, 1, 2); +// self.upsert(domain2, ip2); +// +// // update upsert, for sub-sub +// let domain2 = &format!("test.{}", self.dyn_root_zone); +// let ip2 = Ipv4Addr::new(1, 1, 1, 3); +// self.upsert(domain2, ip2); } } @@ -235,9 +163,5 @@ pub async fn server() -> ServerFuture { server.register_listener(tcp_listener, tcp_request_timeout); info!("awaiting DNS connections..."); - let domain3 = &format!("question.{}", dns_manager.dyn_root_zone); - let ip3 = Ipv4Addr::new(1, 1, 1, 5); - dns_manager.upsert(domain3, ip3); - server } From 24afbe168e4c58aaf2bfb69afda81d050ef5c3fa Mon Sep 17 00:00:00 2001 From: notplants Date: Mon, 10 May 2021 13:20:43 +0200 Subject: [PATCH 06/18] Working on using trust client --- Cargo.toml | 11 ++++++++++- bash/nsupdate.sh | 37 +++++++++++++++++++++++++++++++++++++ src/client.rs | 47 +++++++++++++++++++++++++++++++++++++++++++++++ src/dns.rs | 11 ++++++++--- 4 files changed, 102 insertions(+), 4 deletions(-) create mode 100755 bash/nsupdate.sh create mode 100644 src/client.rs diff --git a/Cargo.toml b/Cargo.toml index c69e78f..155e078 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,4 +20,13 @@ trust-dns-client = "0.20.2" rocket = { git = "https://github.com/SergioBenitez/Rocket", branch = "master" } rocket_contrib = { git = "https://github.com/SergioBenitez/Rocket", branch = "master" } serde = "1.0.125" -dotenv = "0.15.0" \ No newline at end of file +dotenv = "0.15.0" + + +[[bin]] +name = "client" +path = "src/client.rs" + +[[bin]] +name = "dns" +path = "src/main.rs" \ No newline at end of file diff --git a/bash/nsupdate.sh b/bash/nsupdate.sh new file mode 100755 index 0000000..69096e6 --- /dev/null +++ b/bash/nsupdate.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +ECHO=$(which echo) +NSUPDATE=$(which nsupdate) + +# Set the DNS entry you want to update, please notice the final dot. +HOST="test.dyn.commoninternet.net" + +# Set the key provided by your DNS administrator +KEY="/etc/named/Kmydomain.com.+157+19553.key" + +# Set the DNS server name or IP +#SERVER="dyn.local:12323" +SERVER="dyn.local 12323" + +# Set the zone to modify, it can be any zone previous key has permissions to modify +ZONE="dyn.commoninternet.net" + +# Get your public IP address in the quickest and fanciest +# way to if you have bind-tools installed +#IP=`dig TXT +short o-o.myaddr.l.google.com @ns1.google.com | awk -F'"' '{ print $2}'` +#OLDIP=`dig $HOST +short @8.8.8.8` +IP="1.1.1.9" +OLDIP="0.0.0.0" + +if [ "$IP" != "$OLDIP" ]; +then + $ECHO "server $SERVER" > /tmp/nsupdate + $ECHO "debug yes" >> /tmp/nsupdate + $ECHO "zone $ZONE" >> /tmp/nsupdate +# $ECHO "update delete $HOST" >> /tmp/nsupdate + $ECHO "update add $HOST 600 A $IP" >> /tmp/nsupdate + $ECHO "send" >> /tmp/nsupdate +else + $ECHO "No update needed, exiting..." +fi +$NSUPDATE -k ${KEY} -v /tmp/nsupdate \ No newline at end of file diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..09b39d7 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,47 @@ +#![feature(proc_macro_hygiene, decl_macro)] + +#[macro_use] +extern crate rocket; + +use futures::try_join; +use std::io; +use tokio::task; + + +use std::net::Ipv4Addr; +use std::str::FromStr; +use trust_dns_client::client::{Client, SyncClient}; +use trust_dns_client::udp::UdpClientConnection; +use trust_dns_client::op::DnsResponse; +use trust_dns_client::rr::{DNSClass, Name, RData, Record, RecordType}; + + +#[tokio::main] +async fn main() { + + let address = "dyn.local:12323".parse().unwrap(); + let conn = UdpClientConnection::new(address).unwrap(); + let client = SyncClient::new(conn); + + // Specify the name, note the final '.' which specifies it's an FQDN + let name = Name::from_str("www.example.com.").unwrap(); + + // NOTE: see 'Setup a connection' example above + // Send the query and get a message response, see RecordType for all supported options + let response: DnsResponse = client.query(&name, DNSClass::IN, RecordType::A).unwrap(); + + // Messages are the packets sent between client and server in DNS, DnsResonse's can be + // dereferenced to a Message. There are many fields to a Message, It's beyond the scope + // of these examples to explain them. See trust_dns::op::message::Message for more details. + // generally we will be interested in the Message::answers + let answers: &[Record] = response.answers(); + + // Records are generic objects which can contain any data. + // In order to access it we need to first check what type of record it is + // In this case we are interested in A, IPv4 address + if let &RData::A(ref ip) = answers[0].rdata() { + assert_eq!(*ip, Ipv4Addr::new(93, 184, 216, 34)) + } else { + assert!(false, "unexpected result") + } +} diff --git a/src/dns.rs b/src/dns.rs index 6290746..91b0e29 100644 --- a/src/dns.rs +++ b/src/dns.rs @@ -56,12 +56,13 @@ impl DnsManager { authority_records } - fn upsert_domain(mut authority: InMemoryAuthority, domain: String, ip: Ipv4Addr) { + fn upsert_domain(mut authority: InMemoryAuthority, domain: String, ip: Ipv4Addr) -> InMemoryAuthority { let dyn_name = Name::from_str(&domain).unwrap(); let dyn_ttl = 60; let dyn_rdata = RData::A(ip); let dyn_record = Record::from_rdata(dyn_name, dyn_ttl, dyn_rdata); authority.upsert(dyn_record, authority.serial()); + authority } fn build_catalog(&mut self) { @@ -72,7 +73,7 @@ impl DnsManager { let authority_allow_axfr = false; // first create an authority for root_dyn_zone - let authority = InMemoryAuthority::new( + let mut authority = InMemoryAuthority::new( authority_name.clone(), authority_records, authority_zone_type, @@ -83,7 +84,11 @@ impl DnsManager { // then upsert records into the authority for all records in database let domain1 = format!("test.{}", self.dyn_root_zone); let ip1 = Ipv4Addr::new(1, 1, 1, 1); - DnsManager::upsert_domain(authority, domain1, ip1); + authority = DnsManager::upsert_domain(authority, domain1, ip1); + + let domain2 = format!("peach.{}", self.dyn_root_zone); + let ip2 = Ipv4Addr::new(1, 1, 1, 3); + authority = DnsManager::upsert_domain(authority, domain2, ip2); // finally put the authority into the catalog self.catalog.upsert( From bbbc2d4bf93eababfbd8228ca6f62485d8162e39 Mon Sep 17 00:00:00 2001 From: notplants Date: Mon, 10 May 2021 13:21:04 +0200 Subject: [PATCH 07/18] Working on using trust client --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 09b39d7..d94b39f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -19,7 +19,7 @@ use trust_dns_client::rr::{DNSClass, Name, RData, Record, RecordType}; #[tokio::main] async fn main() { - let address = "dyn.local:12323".parse().unwrap(); + let address = "127.0.0.1:12323".parse().unwrap(); let conn = UdpClientConnection::new(address).unwrap(); let client = SyncClient::new(conn); From 88145aac5c5d2ed3c52a1c2d36bc78895d652984 Mon Sep 17 00:00:00 2001 From: notplants Date: Mon, 10 May 2021 13:23:57 +0200 Subject: [PATCH 08/18] Working on client' --- src/client.rs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/client.rs b/src/client.rs index d94b39f..e641655 100644 --- a/src/client.rs +++ b/src/client.rs @@ -16,15 +16,14 @@ use trust_dns_client::op::DnsResponse; use trust_dns_client::rr::{DNSClass, Name, RData, Record, RecordType}; -#[tokio::main] -async fn main() { +fn main() { let address = "127.0.0.1:12323".parse().unwrap(); let conn = UdpClientConnection::new(address).unwrap(); let client = SyncClient::new(conn); // Specify the name, note the final '.' which specifies it's an FQDN - let name = Name::from_str("www.example.com.").unwrap(); + let name = Name::from_str("peach.dyn.commoninternet.net").unwrap(); // NOTE: see 'Setup a connection' example above // Send the query and get a message response, see RecordType for all supported options @@ -39,9 +38,5 @@ async fn main() { // Records are generic objects which can contain any data. // In order to access it we need to first check what type of record it is // In this case we are interested in A, IPv4 address - if let &RData::A(ref ip) = answers[0].rdata() { - assert_eq!(*ip, Ipv4Addr::new(93, 184, 216, 34)) - } else { - assert!(false, "unexpected result") - } + println!("found: {:?}", answers[0].rdata()) } From 5f2ea71f03612f1cb94092dfc7d64f668595dee5 Mon Sep 17 00:00:00 2001 From: notplants Date: Tue, 11 May 2021 07:49:07 +0200 Subject: [PATCH 09/18] Create record not yet implemented --- src/client.rs | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/client.rs b/src/client.rs index e641655..6d4109a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -13,11 +13,14 @@ use std::str::FromStr; use trust_dns_client::client::{Client, SyncClient}; use trust_dns_client::udp::UdpClientConnection; use trust_dns_client::op::DnsResponse; -use trust_dns_client::rr::{DNSClass, Name, RData, Record, RecordType}; +use trust_dns_client::op::update_message; +use trust_dns_client::rr::{DNSClass, Name, RData, Record, RecordType, RecordSet}; +use trust_dns_server::authority::{ + AuthLookup, Authority, LookupError, MessageRequest, UpdateResult, +}; -fn main() { - +fn simple_test() { let address = "127.0.0.1:12323".parse().unwrap(); let conn = UdpClientConnection::new(address).unwrap(); let client = SyncClient::new(conn); @@ -40,3 +43,24 @@ fn main() { // In this case we are interested in A, IPv4 address println!("found: {:?}", answers[0].rdata()) } + + +fn main() { + + let address = "127.0.0.1:12323".parse().unwrap(); + let conn = UdpClientConnection::new(address).unwrap(); + let client = SyncClient::new(conn); + + + // Specify the name, note the final '.' which specifies it's an FQDN + let name = Name::from_str("up.dyn.commoninternet.net").unwrap(); + + let record = Record::from_rdata(name.clone(), 8, RData::A(Ipv4Addr::new(127, 0, 0, 10))); + let rrset: RecordSet = record.clone().into(); + let zone_origin = Name::from_str("dyn.commoninternet.net").unwrap(); + + let response: DnsResponse = client.create(rrset, zone_origin).unwrap(); + println!("response: {:?}", response); + + // this also produces a response code of 4 (not yet implemented) +} From f4d6b31125269b271660f874eba5947c24ca66e6 Mon Sep 17 00:00:00 2001 From: notplants Date: Wed, 12 May 2021 16:56:49 +0200 Subject: [PATCH 10/18] Working bind --- bash/ns1.sh | 16 ++++++++++++++++ bind_config/setting-up-bind.md | 35 ++++++++++++++++++++++++++++++++++ src/client.rs | 31 +++++++++++++++++++----------- 3 files changed, 71 insertions(+), 11 deletions(-) create mode 100755 bash/ns1.sh create mode 100644 bind_config/setting-up-bind.md diff --git a/bash/ns1.sh b/bash/ns1.sh new file mode 100755 index 0000000..95459f9 --- /dev/null +++ b/bash/ns1.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +MYIP="1.1.1.9" + +KEY=ddns.key +NS=ns.commoninternet.net +DOMAIN=test2.time.commoninternet.net. +ZONE=time.commoninternet.net + +nsupdate -k $KEY -v << EOF +server $NS +zone $ZONE +update delete $DOMAIN A +update add $DOMAIN 30 A $MYIP +send +EOF diff --git a/bind_config/setting-up-bind.md b/bind_config/setting-up-bind.md new file mode 100644 index 0000000..1713d99 --- /dev/null +++ b/bind_config/setting-up-bind.md @@ -0,0 +1,35 @@ + + +Add the following to /etc/bind/named.conf.local: +``` +key "ddns-key.dyn.commoninternet.net" { + algorithm hmac-sha256; + secret "yoursecrethere"; +}; + +zone "dyn.commoninternet.net" { +type master; +file "/var/lib/bind/dyn.commoninternet.net"; + update-policy { + grant ddns-key.dyn.commoninternet.net subdomain dyn.commoninternet.net; + }; +}; +``` + + +Add the following to /var/lib/bind/dyn.commoninternet.net: +``` +$ORIGIN . +$TTL 30 ; 30 seconds +dyn.commoninternet.net IN SOA ns.commoninternet.net. root.commoninternet.net. ( + 2016062801 ; serial + 3600 ; refresh (1 hour) + 600 ; retry (10 minutes) + 2600 ; expire (43 minutes 20 seconds) + 30 ; minimum (30 seconds) + ) + NS ns.commoninternet.net. +``` +Note that this file needs to be in /var/lib/bind for bind to have proper write permissions. + +You can then add, delete and update subdomains using nsupdate. diff --git a/src/client.rs b/src/client.rs index 6d4109a..1ef25be 100644 --- a/src/client.rs +++ b/src/client.rs @@ -12,6 +12,7 @@ use std::net::Ipv4Addr; use std::str::FromStr; use trust_dns_client::client::{Client, SyncClient}; use trust_dns_client::udp::UdpClientConnection; +use trust_dns_client::tcp::TcpClientConnection; use trust_dns_client::op::DnsResponse; use trust_dns_client::op::update_message; use trust_dns_client::rr::{DNSClass, Name, RData, Record, RecordType, RecordSet}; @@ -21,16 +22,21 @@ use trust_dns_server::authority::{ fn simple_test() { - let address = "127.0.0.1:12323".parse().unwrap(); +// let address = "127.0.0.1:12323".parse().unwrap(); + let address = "167.99.136.83:53".parse().unwrap(); let conn = UdpClientConnection::new(address).unwrap(); let client = SyncClient::new(conn); +// let conn = TcpClientConnection::new(address).unwrap(); +// let client = SyncClient::new(conn); // Specify the name, note the final '.' which specifies it's an FQDN - let name = Name::from_str("peach.dyn.commoninternet.net").unwrap(); + let name = Name::from_str("time.commoninternet.net.").unwrap(); // NOTE: see 'Setup a connection' example above // Send the query and get a message response, see RecordType for all supported options + println!("++ making query"); let response: DnsResponse = client.query(&name, DNSClass::IN, RecordType::A).unwrap(); + println!("++ received response"); // Messages are the packets sent between client and server in DNS, DnsResonse's can be // dereferenced to a Message. There are many fields to a Message, It's beyond the scope @@ -44,23 +50,26 @@ fn simple_test() { println!("found: {:?}", answers[0].rdata()) } - -fn main() { - - let address = "127.0.0.1:12323".parse().unwrap(); +fn update_test() { + let address = "167.99.136.83:53".parse().unwrap(); let conn = UdpClientConnection::new(address).unwrap(); let client = SyncClient::new(conn); // Specify the name, note the final '.' which specifies it's an FQDN - let name = Name::from_str("up.dyn.commoninternet.net").unwrap(); + let name = Name::from_str("test.time.commoninternet.net").unwrap(); let record = Record::from_rdata(name.clone(), 8, RData::A(Ipv4Addr::new(127, 0, 0, 10))); let rrset: RecordSet = record.clone().into(); - let zone_origin = Name::from_str("dyn.commoninternet.net").unwrap(); + let zone_origin = Name::from_str("time.commoninternet.net").unwrap(); - let response: DnsResponse = client.create(rrset, zone_origin).unwrap(); + let response: DnsResponse = client.create(rrset, zone_origin).expect("failed to create record"); println!("response: {:?}", response); - - // this also produces a response code of 4 (not yet implemented) +} + + +fn main() { + +// simple_test(); + update_test(); } From 63683039225bd64ec7e1bc6730ed6efe663686f6 Mon Sep 17 00:00:00 2001 From: notplants Date: Sat, 15 May 2021 08:57:32 +0200 Subject: [PATCH 11/18] Using nsupdate and bind --- Cargo.toml | 2 +- src/client.rs | 39 ++++-------- src/dns.rs | 172 -------------------------------------------------- src/http.rs | 45 +++++-------- src/main.rs | 29 +++------ 5 files changed, 39 insertions(+), 248 deletions(-) delete mode 100644 src/dns.rs diff --git a/Cargo.toml b/Cargo.toml index 155e078..c132bf9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,5 +28,5 @@ name = "client" path = "src/client.rs" [[bin]] -name = "dns" +name = "main" path = "src/main.rs" \ No newline at end of file diff --git a/src/client.rs b/src/client.rs index 1ef25be..7dc8798 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,8 +1,3 @@ -#![feature(proc_macro_hygiene, decl_macro)] - -#[macro_use] -extern crate rocket; - use futures::try_join; use std::io; use tokio::task; @@ -21,33 +16,23 @@ use trust_dns_server::authority::{ }; -fn simple_test() { -// let address = "127.0.0.1:12323".parse().unwrap(); +pub fn check_domain_available(domain: &str) -> bool { let address = "167.99.136.83:53".parse().unwrap(); let conn = UdpClientConnection::new(address).unwrap(); let client = SyncClient::new(conn); -// let conn = TcpClientConnection::new(address).unwrap(); -// let client = SyncClient::new(conn); + let name = Name::from_str(domain).unwrap(); - // Specify the name, note the final '.' which specifies it's an FQDN - let name = Name::from_str("time.commoninternet.net.").unwrap(); - - // NOTE: see 'Setup a connection' example above - // Send the query and get a message response, see RecordType for all supported options - println!("++ making query"); + info!("++ making query {:?}", domain); let response: DnsResponse = client.query(&name, DNSClass::IN, RecordType::A).unwrap(); - println!("++ received response"); - - // Messages are the packets sent between client and server in DNS, DnsResonse's can be - // dereferenced to a Message. There are many fields to a Message, It's beyond the scope - // of these examples to explain them. See trust_dns::op::message::Message for more details. - // generally we will be interested in the Message::answers + info!("++ received response"); let answers: &[Record] = response.answers(); - // Records are generic objects which can contain any data. - // In order to access it we need to first check what type of record it is - // In this case we are interested in A, IPv4 address - println!("found: {:?}", answers[0].rdata()) + if answers.len() > 0 { + info!("found: {:?}", answers[0].rdata()); + true + } else { + false + } } fn update_test() { @@ -70,6 +55,6 @@ fn update_test() { fn main() { -// simple_test(); - update_test(); + check_domain_available("test"); +// update_test(); } diff --git a/src/dns.rs b/src/dns.rs deleted file mode 100644 index 91b0e29..0000000 --- a/src/dns.rs +++ /dev/null @@ -1,172 +0,0 @@ -use std::collections::BTreeMap; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use std::str::FromStr; -use std::sync::{Arc, RwLock}; -use std::time::Duration; -use tokio::net::TcpListener; -use tokio::net::UdpSocket; -use trust_dns_client::rr::rdata::soa::SOA; -use trust_dns_client::rr::{LowerName, Name, RData, Record, RecordSet, RecordType, RrKey}; -use trust_dns_server::authority::{Catalog, ZoneType}; -use trust_dns_server::server::ServerFuture; -use trust_dns_server::store::in_memory::InMemoryAuthority; -use std::env; -use dotenv; - -static DEFAULT_TCP_REQUEST_TIMEOUT: u64 = 5; - - -struct DnsManager { - catalog: Catalog, - dyn_root_zone: String, -} - -impl DnsManager { - pub fn new(dyn_root_zone: String) -> DnsManager { - - let catalog: Catalog = Catalog::new(); - - return DnsManager { - catalog, - dyn_root_zone, - }; - } - - fn get_initial_records(domain: &str) -> BTreeMap { - let authority_name = Name::from_str(domain).unwrap(); - let soa_serial = 1; - let soa_name = Name::from_str(domain).unwrap(); - let soa_rdata = RData::SOA(SOA::new( - Name::from_str(domain).unwrap(), // mname - Name::from_str(&format!("root.{}", domain)).unwrap(), // rname - soa_serial, // serial - 604800, // refresh - 86400, // retry - 2419200, // expire - 86400, // negtive cache ttl - )); - let mut soa_record_set = RecordSet::new(&soa_name, RecordType::SOA, soa_serial); - soa_record_set.add_rdata(soa_rdata); - let soa_rr_key = RrKey::new( - LowerName::new(&authority_name), - soa_record_set.record_type(), - ); - let mut authority_records = BTreeMap::new(); - authority_records.insert(soa_rr_key, soa_record_set); - authority_records - } - - fn upsert_domain(mut authority: InMemoryAuthority, domain: String, ip: Ipv4Addr) -> InMemoryAuthority { - let dyn_name = Name::from_str(&domain).unwrap(); - let dyn_ttl = 60; - let dyn_rdata = RData::A(ip); - let dyn_record = Record::from_rdata(dyn_name, dyn_ttl, dyn_rdata); - authority.upsert(dyn_record, authority.serial()); - authority - } - - fn build_catalog(&mut self) { - let authority_records = DnsManager::get_initial_records(&self.dyn_root_zone); - let authority_name = Name::from_str(&self.dyn_root_zone).unwrap(); - - let authority_zone_type = ZoneType::Master; - let authority_allow_axfr = false; - - // first create an authority for root_dyn_zone - let mut authority = InMemoryAuthority::new( - authority_name.clone(), - authority_records, - authority_zone_type, - authority_allow_axfr, - ) - .unwrap(); - - // then upsert records into the authority for all records in database - let domain1 = format!("test.{}", self.dyn_root_zone); - let ip1 = Ipv4Addr::new(1, 1, 1, 1); - authority = DnsManager::upsert_domain(authority, domain1, ip1); - - let domain2 = format!("peach.{}", self.dyn_root_zone); - let ip2 = Ipv4Addr::new(1, 1, 1, 3); - authority = DnsManager::upsert_domain(authority, domain2, ip2); - - // finally put the authority into the catalog - self.catalog.upsert( - LowerName::new(&authority_name), - Box::new(Arc::new(RwLock::new(authority))), - ); - } - - - fn upsert_test(&mut self) { - - // first insert the authority for the root dyn zone - self.build_catalog(); - - // second upsert, for sub-sub - - // third upsert, for sub-sub -// let domain2 = &format!("peach.{}", self.dyn_root_zone); -// let ip2 = Ipv4Addr::new(1, 1, 1, 2); -// self.upsert(domain2, ip2); -// -// // update upsert, for sub-sub -// let domain2 = &format!("test.{}", self.dyn_root_zone); -// let ip2 = Ipv4Addr::new(1, 1, 1, 3); -// self.upsert(domain2, ip2); - - } -} - - -pub async fn server() -> ServerFuture { - info!("Trust-DNS {} starting", trust_dns_server::version()); - - dotenv::from_path("/etc/peach-dyndns.conf").ok(); - let dyn_root_zone = env::var("DYN_ROOT_ZONE").expect("DYN_ROOT_ZONE not set"); - let mut dns_manager = DnsManager::new(dyn_root_zone.to_string()); - -// // first insert -// dns_manager.upsert( -// "test.dyn.peachcloud.org".to_string(), -// Ipv4Addr::new(1, 1, 1, 1), -// ); -// -// // second insert -// dns_manager.upsert( -// "test.dyn.peachcloud.org".to_string(), -// Ipv4Addr::new(1, 1, 1, 3), -// ); -// -// // third insert -// dns_manager.upsert( -// "peach.dyn.peachcloud.org".to_string(), -// Ipv4Addr::new(1, 1 , 2, 3), -// ); - - dns_manager.upsert_test(); - - let ip_addr = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)); - let listen_port: u16 = 12323; - let tcp_request_timeout = Duration::from_secs(DEFAULT_TCP_REQUEST_TIMEOUT); - - let sock_addr = SocketAddr::new(ip_addr, listen_port); - let udp_socket = UdpSocket::bind(&sock_addr) - .await - .expect("could not bind udp socket"); - let tcp_listener = TcpListener::bind(&sock_addr) - .await - .expect("could not bind tcp listener"); - - let mut server = ServerFuture::new(dns_manager.catalog); - - // load all the listeners - info!("DNS server listening for UDP on {:?}", udp_socket); - server.register_socket(udp_socket); - - info!("DNS server listening for TCP on {:?}", tcp_listener); - server.register_listener(tcp_listener, tcp_request_timeout); - info!("awaiting DNS connections..."); - - server -} diff --git a/src/http.rs b/src/http.rs index 14e16e5..c258be9 100644 --- a/src/http.rs +++ b/src/http.rs @@ -8,43 +8,32 @@ */ use rocket_contrib::json::Json; use serde::Deserialize; +use std::thread; +use crate::client::check_domain_available; #[get("/")] -fn index() -> &'static str { +pub fn index() -> &'static str { "This is the peach-dyn-dns server." } #[derive(Deserialize, Debug)] -struct RegisterDomainPost { +pub struct RegisterDomainPost { domain: String, } #[post("/register-domain", data = "")] -fn register_domain(data: Json) -> &'static str { +pub async fn register_domain(data: Json) -> &'static str { info!("++ post request to register new domain: {:?}", data); - "New domain registered" // TODO: return secret -} - -#[derive(Deserialize, Debug)] -struct UpdateDomainPost { - domain: String, - secret: String, -} - -#[post("/update-domain", data = "")] -fn update_domain(data: Json) -> &'static str { - info!("++ post request to update domain: {:?}", data); - "Updating domain" // TODO: validate, then do it -} - -pub async fn server() { - - let rocket_result= rocket::build() - .mount("/", routes![index, register_domain, update_domain]) - .launch() - .await; - - if let Err(err) = rocket_result { - error!("++ error launching rocket server: {:?}", err); + // TODO: first confirm domain is in the right format ("*.dyn.peachcloud.org") + let handle = thread::spawn(move || { + let domain_already_exists = check_domain_available(&data.domain); + domain_already_exists + }); + let domain_already_exists = handle.join().unwrap(); + if domain_already_exists { + "can't register domain already exists" + } else { + // TODO: use bash to generate a tsig key, update bind config, and then return the secret + "New domain registered" } -} +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 690daa3..f1e681a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,33 +6,22 @@ extern crate rocket; use futures::try_join; use std::io; use tokio::task; +use crate::http::{index, register_domain}; mod cli; -mod dns; mod http; +mod client; #[tokio::main] async fn main() { - let _args = cli::args().expect("error parsing args"); - // create future for dns and http servers - let dns_future = task::spawn(dns::server()); - let http_future = task::spawn(http::server()); + let rocket_result= rocket::build() + .mount("/", routes![index, register_domain]) + .launch() + .await; - // join futures - let result = try_join!(dns_future, http_future); - - match result { - Err(e) => { - io::Error::new( - io::ErrorKind::Interrupted, - "Server stopping due to interruption", - ); - error!("server failure: {}", e); - } - Ok(_val) => { - info!("we're stopping for some unexpected reason"); - } + if let Err(err) = rocket_result { + error!("++ error launching rocket server: {:?}", err); } - info!("we're stopping for some unexpected reason"); + } From ff87de1150570388a7ebdb00ad3fd5ddc49f2e3b Mon Sep 17 00:00:00 2001 From: notplants Date: Sun, 16 May 2021 13:18:55 +0200 Subject: [PATCH 12/18] Slowly working on updating bind configs using rust --- Cargo.toml | 4 ++ bash/create_subdomain.sh | 12 +++++ bind_config/bind-subdomain.md | 53 +++++++++++++++++++ bind_config/setting-up-bind.md | 2 +- generate_zone.sh | 2 + src/utils.rs | 93 ++++++++++++++++++++++++++++++++++ 6 files changed, 165 insertions(+), 1 deletion(-) create mode 100755 bash/create_subdomain.sh create mode 100644 bind_config/bind-subdomain.md create mode 100755 generate_zone.sh create mode 100644 src/utils.rs diff --git a/Cargo.toml b/Cargo.toml index c132bf9..4ab7b01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,10 @@ dotenv = "0.15.0" name = "client" path = "src/client.rs" +[[bin]] +name = "zone" +path = "src/utils.rs" + [[bin]] name = "main" path = "src/main.rs" \ No newline at end of file diff --git a/bash/create_subdomain.sh b/bash/create_subdomain.sh new file mode 100755 index 0000000..376a8ec --- /dev/null +++ b/bash/create_subdomain.sh @@ -0,0 +1,12 @@ +# For each subdomain, +# - generate a new ddns key (tsig-keygen -a hmac-md5 {{subdomain}}.dyn.commoninternet.net) and append it to /etc/bind/dyn.commoninternet.net.keys +# - add a zone section to /etc/bind/named.conf.local, associating the key with the subdomain +# - add a minimal zone file to /var/lib/bind/subdomain.dyn.commoninternet.net +# - reload bind and return the secret key to the client + +SUBDOMAIN=$1 +BASE_DOMAIN=dyn.commoninternet.net +FULL_DOMAIN="${SUBDOMAIN}.${BASE_DOMAIN}" +echo "[generating zone for ${FULL_DOMAIN}]" + +tsig-keygen -a hmac-md5 {{subdomain}}.dyn.commoninternet.net \ No newline at end of file diff --git a/bind_config/bind-subdomain.md b/bind_config/bind-subdomain.md new file mode 100644 index 0000000..aef4d7c --- /dev/null +++ b/bind_config/bind-subdomain.md @@ -0,0 +1,53 @@ + + + +Add the following to /etc/bind/named.conf.local: +``` +key "ddns-key.dyn.commoninternet.net" { + algorithm hmac-sha256; + secret "yoursecrethere"; +}; + +zone "dyn.commoninternet.net" { +type master; +file "/var/lib/bind/dyn.commoninternet.net"; + update-policy { + grant ddns-key.dyn.commoninternet.net subdomain dyn.commoninternet.net; + }; +}; +``` + +For each subdomain, +- generate a new ddns key (tsig-keygen -a hmac-md5 {{subdomain}}.dyn.commoninternet.net) and append it to /etc/bind/dyn.commoninternet.net.keys +- add a zone section to named.conf.local, associating the key with the subdomain [B] +- add a zone file to /var/lib/bind/subdomain.dyn.commoninternet.net [C] +- reload bind and return the secret key to the client + +Add the following to /var/lib/bind/{{subdomain}}.dyn.commoninternet.net: [C] +``` +$ORIGIN . +$TTL 30 ; 30 seconds +{{subdomain}}.dyn.commoninternet.net IN SOA ns.commoninternet.net. root.commoninternet.net. ( + 2016062801 ; serial + 3600 ; refresh (1 hour) + 600 ; retry (10 minutes) + 2600 ; expire (43 minutes 20 seconds) + 30 ; minimum (30 seconds) + ) + NS ns.commoninternet.net. +``` + +Append the following to /etc/bind/named.conf.local: [B] +``` +zone "{{subdomain}}.dyn.commoninternet.net" { +type master; +file "/var/lib/bind/{{subdomain}}.dyn.commoninternet.net"; + update-policy { + grant {{subdomain}}.dyn.commoninternet.net self {{subdomain}}.dyn.commoninternet.net; + }; +}; +``` + + +Questions: +- an easy way to delete a subdomain? diff --git a/bind_config/setting-up-bind.md b/bind_config/setting-up-bind.md index 1713d99..4cd8393 100644 --- a/bind_config/setting-up-bind.md +++ b/bind_config/setting-up-bind.md @@ -1,5 +1,6 @@ + Add the following to /etc/bind/named.conf.local: ``` key "ddns-key.dyn.commoninternet.net" { @@ -16,7 +17,6 @@ file "/var/lib/bind/dyn.commoninternet.net"; }; ``` - Add the following to /var/lib/bind/dyn.commoninternet.net: ``` $ORIGIN . diff --git a/generate_zone.sh b/generate_zone.sh new file mode 100755 index 0000000..01fbd3c --- /dev/null +++ b/generate_zone.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +cargo run --bin zone -- -vvv \ No newline at end of file diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..afed411 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,93 @@ +/* For each subdomain, +- generate a new ddns key (tsig-keygen -a hmac-md5 {{subdomain}}.dyn.commoninternet.net) and append it to /etc/bind/dyn.commoninternet.net.keys +- add a zone section to /etc/bind/named.conf.local, associating the key with the subdomain +- add a minimal zone file to /var/lib/bind/subdomain.dyn.commoninternet.net +- reload bind and return the secret key to the client +*/ +use std::process::Command; +use std::io::Error; +use std::io::Write; +use std::string::FromUtf8Error; +use std::{fs::OpenOptions}; +use std::fs::File; + +const BASE_DOMAIN : &str = "dyn.commoninternet.net"; + + +#[derive(Debug)] +pub enum PeachDynError { + GenerateTsigIoError(std::io::Error), + GenerateTsigParseError(std::string::FromUtf8Error), +} + +impl From for PeachDynError { + fn from(err: std::io::Error) -> PeachDynError { + PeachDynError::GenerateTsigIoError(err) + } +} + +impl From for PeachDynError { + fn from(err: std::string::FromUtf8Error) -> PeachDynError { + PeachDynError::GenerateTsigParseError(err) + } +} + + +/// helper function to generate a TSIG key file +pub fn generate_tsig_key(full_domain: &str) -> Result { + let output = Command::new("/usr/sbin/tsig-keygen") + .arg("-a") + .arg("hmac-md5") + .arg(full_domain) + .output()?; + let key_file_text = String::from_utf8(output.stdout)?; + Ok(key_file_text) +} + + + +fn generate_zone(subdomain: &str) { + + let full_domain=format!("{}.{}", subdomain, BASE_DOMAIN); + println!("[generating zone for {}]", subdomain); + + // generate key_file_text + let key_file_text = generate_tsig_key(&full_domain).unwrap(); + println!("key_file_text: {}", key_file_text); + + // write key_file_text to file + let key_file_path = "/etc/bind/dyn.commoninternet.net.keys"; + let mut file = OpenOptions::new() + .append(true) + .open(key_file_path).unwrap(); + if let Err(e) = writeln!(file, "{}", key_file_text) { + eprintln!("Couldn't write to file: {}", e); + } + + // append to named.local.conf + let bind_conf_path = "/etc/bind/named.local.conf"; + let mut file = OpenOptions::new() + .append(true) + .open(bind_conf_path).unwrap(); + let zone_section_text = format!("\ + zone \"{full_domain}\" {{ + type master; + file \"/var/lib/bind/{full_domain}\"; + update-policy {{ + grant {full_domain} self {full_domain}; + }}; + }}; + ", full_domain=full_domain); + if let Err(e) = writeln!(file, "{}", zone_section_text) { + eprintln!("Couldn't write to file: {}", e); + } + + + +} + + + +fn main() { + generate_zone("blue"); +} From 03745f4fb22d26ba86f5b127f7651804d37aaac6 Mon Sep 17 00:00:00 2001 From: notplants Date: Mon, 17 May 2021 20:15:36 +0200 Subject: [PATCH 13/18] Integration between bind and dns is working --- .gitignore | 1 + Cargo.lock | 338 +++++++++++++++++- Cargo.toml | 3 +- README.md | 10 +- ... => setting-up-bind-specific-subdomain.md} | 0 ...etting-up-bind-with-wildcard-subdomain.md} | 0 bind_config/setup.md | 17 + ns_tests/grayns.sh | 16 + ns_tests/greenns.sh | 16 + ns_tests/ns1.sh | 16 + src/client.rs | 29 +- src/generate_zone.rs | 171 +++++++++ src/http.rs | 28 +- src/main.rs | 9 +- src/utils.rs | 93 ----- templates/zonefile.tera | 10 + 16 files changed, 626 insertions(+), 131 deletions(-) rename bind_config/{bind-subdomain.md => setting-up-bind-specific-subdomain.md} (100%) rename bind_config/{setting-up-bind.md => setting-up-bind-with-wildcard-subdomain.md} (100%) create mode 100644 bind_config/setup.md create mode 100755 ns_tests/grayns.sh create mode 100755 ns_tests/greenns.sh create mode 100755 ns_tests/ns1.sh create mode 100644 src/generate_zone.rs delete mode 100644 src/utils.rs create mode 100644 templates/zonefile.tera diff --git a/.gitignore b/.gitignore index 53eaa21..7bd65eb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target **/*.rs.bk +ns_tests/*.key diff --git a/Cargo.lock b/Cargo.lock index 8f8c8ee..acd3f31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -135,12 +135,48 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "block-buffer" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +dependencies = [ + "block-padding", + "byte-tools", + "byteorder", + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +dependencies = [ + "byte-tools", +] + +[[package]] +name = "bstr" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a40b47ad93e1a5404e6c18dec46b628214fee441c70f4ab5d6942142cc268a3d" +dependencies = [ + "memchr", +] + [[package]] name = "bumpalo" version = "3.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + [[package]] name = "byteorder" version = "1.4.3" @@ -194,6 +230,16 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "chrono-tz" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2554a3155fec064362507487171dcc4edc3df60cb10f3a1fb10ed8094822b120" +dependencies = [ + "chrono", + "parse-zoneinfo", +] + [[package]] name = "clap" version = "2.33.3" @@ -277,6 +323,17 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4feb231f0d4d6af81aed15928e58ecf5816aa62a2393e2c82f46973e92a9a278" +dependencies = [ + "autocfg", + "cfg-if 1.0.0", + "lazy_static", +] + [[package]] name = "data-encoding" version = "2.3.2" @@ -295,6 +352,12 @@ dependencies = [ "syn 1.0.71", ] +[[package]] +name = "deunicode" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "850878694b7933ca4c9569d30a34b55031b9b139ee1fc7b94a527c4ef960d690" + [[package]] name = "devise" version = "0.3.0" @@ -325,6 +388,15 @@ dependencies = [ "syn 1.0.71", ] +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array", +] + [[package]] name = "discard" version = "1.0.4" @@ -424,6 +496,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" + [[package]] name = "figment" version = "0.10.5" @@ -587,6 +665,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + [[package]] name = "getrandom" version = "0.2.2" @@ -610,6 +697,30 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +[[package]] +name = "globset" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c152169ef1e421390738366d2f796655fec62621dabbd0fd476f905934061e4a" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags", + "ignore", + "walkdir", +] + [[package]] name = "h2" version = "0.3.3" @@ -687,6 +798,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05842d0d43232b23ccb7060ecb0f0626922c21f30012e97b767b30afd4a5d4b9" +[[package]] +name = "humansize" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cab2627acfc432780848602f3f558f7e9dd427352224b0d9324025796d2a5e" + [[package]] name = "humantime" version = "1.3.0" @@ -737,6 +854,24 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "ignore" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b287fb45c60bb826a0dc68ff08742b9d88a2fea13d6e0c286b3172065aaf878c" +dependencies = [ + "crossbeam-utils 0.8.4", + "globset", + "lazy_static", + "log", + "memchr", + "regex", + "same-file", + "thread_local", + "walkdir", + "winapi-util", +] + [[package]] name = "indexmap" version = "1.6.2" @@ -862,6 +997,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + [[package]] name = "matches" version = "0.1.8" @@ -1091,6 +1232,12 @@ version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + [[package]] name = "parking_lot" version = "0.9.0" @@ -1142,6 +1289,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "parse-zoneinfo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +dependencies = [ + "regex", +] + [[package]] name = "peach-dyndns-host" version = "0.1.0" @@ -1156,6 +1312,7 @@ dependencies = [ "rocket_contrib", "serde 1.0.125", "structopt", + "tera", "tokio", "tokio-executor", "tokio-tcp", @@ -1193,6 +1350,49 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "pest" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +dependencies = [ + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2 1.0.26", + "quote 1.0.9", + "syn 1.0.71", +] + +[[package]] +name = "pest_meta" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" +dependencies = [ + "maplit", + "pest", + "sha-1", +] + [[package]] name = "pin-project" version = "1.0.7" @@ -1573,6 +1773,15 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scoped-tls" version = "1.0.0" @@ -1672,6 +1881,18 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "sha-1" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" +dependencies = [ + "block-buffer", + "digest", + "fake-simd", + "opaque-debug", +] + [[package]] name = "sha1" version = "0.6.0" @@ -1693,6 +1914,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527" +[[package]] +name = "slug" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373" +dependencies = [ + "deunicode", +] + [[package]] name = "smallvec" version = "0.6.14" @@ -1900,6 +2130,28 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "tera" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b64b021b8d3ab1f59ceae9e6cd1c26c8e7ce0322a9ebfff6c0e22b3b66938935" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand 0.8.3", + "regex", + "serde 1.0.125", + "serde_json", + "slug", + "unic-segment", +] + [[package]] name = "termcolor" version = "1.1.2" @@ -1938,6 +2190,15 @@ dependencies = [ "syn 1.0.71", ] +[[package]] +name = "thread_local" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" +dependencies = [ + "once_cell", +] + [[package]] name = "time" version = "0.1.43" @@ -2038,7 +2299,7 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb2d1b8f4548dbf5e1f7818512e9c406860678f29c300cdf0ebac72d1a3a1671" dependencies = [ - "crossbeam-utils", + "crossbeam-utils 0.7.2", "futures 0.1.31", ] @@ -2070,7 +2331,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09bc590ec4ba8ba87652da2068d150dcada2cfa2e07faae270a5e0409aa51351" dependencies = [ - "crossbeam-utils", + "crossbeam-utils 0.7.2", "futures 0.1.31", "lazy_static", "log", @@ -2256,6 +2517,12 @@ dependencies = [ "unchecked-index", ] +[[package]] +name = "typenum" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" + [[package]] name = "ubyte" version = "0.10.1" @@ -2265,6 +2532,12 @@ dependencies = [ "serde 1.0.125", ] +[[package]] +name = "ucd-trie" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" + [[package]] name = "uncased" version = "0.9.6" @@ -2281,6 +2554,56 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c" +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" +dependencies = [ + "unic-ucd-segment", +] + +[[package]] +name = "unic-ucd-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + [[package]] name = "unicode-bidi" version = "0.3.5" @@ -2353,6 +2676,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi 0.3.9", + "winapi-util", +] + [[package]] name = "want" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index 4ab7b01..5df6d36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ rocket = { git = "https://github.com/SergioBenitez/Rocket", branch = "master" } rocket_contrib = { git = "https://github.com/SergioBenitez/Rocket", branch = "master" } serde = "1.0.125" dotenv = "0.15.0" +tera = "1" [[bin]] @@ -29,7 +30,7 @@ path = "src/client.rs" [[bin]] name = "zone" -path = "src/utils.rs" +path = "src/generate_zone.rs" [[bin]] name = "main" diff --git a/README.md b/README.md index 3f163c6..44ceca8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # peach-dyndns-host -a DNS server to host the names of guests with changing IP addresses +a dynamic DNS server to host the names of guests with changing IP addresses. provides an http API +for updating bind9 configurations. _work in progress_ @@ -15,7 +16,7 @@ cargo run -- -vvv # DEBUG log verbosity in another terminal ```shell -dig @localhost -p 12323 test.dyn.peach.cloud +nslookup blue.dyn.peachcloud.org ns.peachcloud.org ``` or @@ -23,3 +24,8 @@ or ```shell curl http://localhost:3000 ``` + + +## testing + +contains bash scripts for testing and debugging dynamic dns server behavior using nslookup \ No newline at end of file diff --git a/bind_config/bind-subdomain.md b/bind_config/setting-up-bind-specific-subdomain.md similarity index 100% rename from bind_config/bind-subdomain.md rename to bind_config/setting-up-bind-specific-subdomain.md diff --git a/bind_config/setting-up-bind.md b/bind_config/setting-up-bind-with-wildcard-subdomain.md similarity index 100% rename from bind_config/setting-up-bind.md rename to bind_config/setting-up-bind-with-wildcard-subdomain.md diff --git a/bind_config/setup.md b/bind_config/setup.md new file mode 100644 index 0000000..69e2f57 --- /dev/null +++ b/bind_config/setup.md @@ -0,0 +1,17 @@ + + +The following goes into `/etc/sudoers.d/bindctl` to enable peach-dyndns to reload bind. +``` +# +# Allow server to reload bind +# + +# User alias for bind-ctl which can reload bind +User_Alias BIND_CTRL = peach-dynds + +# Command alias for reboot and shutdown +Cmnd_Alias RELOADBIND = /bin/reloadbind + +# Allow BIND_CTRL users to execute RELOADBIND command without password +BIND_CTRL ALL=(ALL) NOPASSWD: RELOADBIND +``` \ No newline at end of file diff --git a/ns_tests/grayns.sh b/ns_tests/grayns.sh new file mode 100755 index 0000000..6175f4a --- /dev/null +++ b/ns_tests/grayns.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +MYIP="1.1.1.55" + +KEY=green.dyn.commoninternet.net.key +NS=ns.commoninternet.net +DOMAIN=gray.dyn.commoninternet.net. +ZONE=gray.dyn.commoninternet.net + +nsupdate -k $KEY -v << EOF +server $NS +zone $ZONE +update delete $DOMAIN A +update add $DOMAIN 30 A $MYIP +send +EOF diff --git a/ns_tests/greenns.sh b/ns_tests/greenns.sh new file mode 100755 index 0000000..1d29c1c --- /dev/null +++ b/ns_tests/greenns.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +MYIP="1.1.1.44" + +KEY=green.dyn.commoninternet.net.key +NS=ns.commoninternet.net +DOMAIN=green.dyn.commoninternet.net. +ZONE=green.dyn.commoninternet.net + +nsupdate -k $KEY -v << EOF +server $NS +zone $ZONE +update delete $DOMAIN A +update add $DOMAIN 30 A $MYIP +send +EOF diff --git a/ns_tests/ns1.sh b/ns_tests/ns1.sh new file mode 100755 index 0000000..98c4b50 --- /dev/null +++ b/ns_tests/ns1.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +MYIP="1.1.1.11" + +KEY=ddns.key +NS=ns.commoninternet.net +DOMAIN=orange.time.commoninternet.net. +ZONE=time.commoninternet.net + +nsupdate -k $KEY -v << EOF +server $NS +zone $ZONE +update delete $DOMAIN A +update add $DOMAIN 30 A $MYIP +send +EOF diff --git a/src/client.rs b/src/client.rs index 7dc8798..28612d3 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2,33 +2,31 @@ use futures::try_join; use std::io; use tokio::task; - use std::net::Ipv4Addr; use std::str::FromStr; use trust_dns_client::client::{Client, SyncClient}; -use trust_dns_client::udp::UdpClientConnection; -use trust_dns_client::tcp::TcpClientConnection; -use trust_dns_client::op::DnsResponse; use trust_dns_client::op::update_message; -use trust_dns_client::rr::{DNSClass, Name, RData, Record, RecordType, RecordSet}; +use trust_dns_client::op::DnsResponse; +use trust_dns_client::rr::{DNSClass, Name, RData, Record, RecordSet, RecordType}; +use trust_dns_client::tcp::TcpClientConnection; +use trust_dns_client::udp::UdpClientConnection; use trust_dns_server::authority::{ AuthLookup, Authority, LookupError, MessageRequest, UpdateResult, }; - -pub fn check_domain_available(domain: &str) -> bool { +pub fn check_domain_available_using_nslookup(domain: &str) -> bool { let address = "167.99.136.83:53".parse().unwrap(); let conn = UdpClientConnection::new(address).unwrap(); let client = SyncClient::new(conn); let name = Name::from_str(domain).unwrap(); - info!("++ making query {:?}", domain); + println!("++ making query {:?}", domain); let response: DnsResponse = client.query(&name, DNSClass::IN, RecordType::A).unwrap(); - info!("++ received response"); + println!("++ received response"); let answers: &[Record] = response.answers(); if answers.len() > 0 { - info!("found: {:?}", answers[0].rdata()); + println!("found: {:?}", answers[0].rdata()); true } else { false @@ -40,7 +38,6 @@ fn update_test() { let conn = UdpClientConnection::new(address).unwrap(); let client = SyncClient::new(conn); - // Specify the name, note the final '.' which specifies it's an FQDN let name = Name::from_str("test.time.commoninternet.net").unwrap(); @@ -48,13 +45,13 @@ fn update_test() { let rrset: RecordSet = record.clone().into(); let zone_origin = Name::from_str("time.commoninternet.net").unwrap(); - let response: DnsResponse = client.create(rrset, zone_origin).expect("failed to create record"); + let response: DnsResponse = client + .create(rrset, zone_origin) + .expect("failed to create record"); println!("response: {:?}", response); } - fn main() { - - check_domain_available("test"); -// update_test(); + check_domain_available_using_nslookup("test"); + // update_test(); } diff --git a/src/generate_zone.rs b/src/generate_zone.rs new file mode 100644 index 0000000..19de29d --- /dev/null +++ b/src/generate_zone.rs @@ -0,0 +1,171 @@ +/* For each subdomain, +- generate a new ddns key (tsig-keygen -a hmac-md5 {{subdomain}}.dyn.commoninternet.net) and append it to /etc/bind/dyn.commoninternet.net.keys +- add a zone section to /etc/bind/named.conf.local, associating the key with the subdomain +- add a minimal zone file to /var/lib/bind/subdomain.dyn.commoninternet.net +- reload bind and return the secret key to the client +*/ +use std::fs::File; +use std::fs::OpenOptions; +use std::io::Write; +use std::process::Command; +use std::string::FromUtf8Error; +use tera::{Tera, Context}; + + +const BASE_DOMAIN: &str = "dyn.commoninternet.net"; + +#[derive(Debug)] +pub enum PeachDynError { + GenerateTsigIoError(std::io::Error), + GenerateTsigParseError(std::string::FromUtf8Error), + DomainAlreadyExistsError(String), + BindConfigurationError(String), +} + +impl From for PeachDynError { + fn from(err: std::io::Error) -> PeachDynError { + PeachDynError::GenerateTsigIoError(err) + } +} + +impl From for PeachDynError { + fn from(err: std::string::FromUtf8Error) -> PeachDynError { + PeachDynError::GenerateTsigParseError(err) + } +} + +/// helper function to generate the text of a TSIG key file +pub fn generate_tsig_key(full_domain: &str) -> Result { + let output = Command::new("/usr/sbin/tsig-keygen") + .arg("-a") + .arg("hmac-md5") + .arg(full_domain) + .output()?; + let key_file_text = String::from_utf8(output.stdout)?; + Ok(key_file_text) +} + +/// helper function which returns true if all three conditions are true +/// - no already extant tsig key for the given domain +/// - no zone file for the given domain in /var/lib/bind +/// - no zone section for the given domain in named.conf.local +pub fn check_domain_available(full_domain: &str) -> bool { + let status1 = Command::new("/bin/grep") + .arg(full_domain) + .arg("/etc/bind/named.conf.local") + .status().expect("error running grep on /etc/bind/named.conf.local"); + let code1 = status1.code().expect("error getting code from grep"); + let status2 = Command::new("/bin/grep") + .arg(full_domain) + .arg("/etc/bind/dyn.commoninternet.net.keys") + .status().expect("error running grep on /etc/bind/dyn.commoninternet.net.keys"); + let code2 = status2.code().expect("error getting code from grep"); + let condition3 = std::path::Path::new(&format!("/var/lib/bind/{}", full_domain)).exists(); + + // domain is only available if domain does not exist in either named.conf.local or dyn.commoninternet.netkeys + // and a file with that name is not found in /var/lib/bind/ + let domain_available = (code1 == 1) & (code2 == 1) & (!condition3); + + // return + domain_available + +} + +/// helper function which generates all necessary bind configuration to serve the given +/// subdomain using dynamic DNS authenticated via a new TSIG key which is unique to that subdomain +/// and thus only the possessor of that key can use nsupdate to modify the records +/// for that subodmain +pub fn generate_zone(subdomain: &str) -> Result { + + // construct full_domain + let full_domain = format!("{}.{}", subdomain, BASE_DOMAIN); + println!("[generating zone for {}]", full_domain); + + // first safety check if the domain is available + let is_available = check_domain_available(&full_domain); + if !is_available { + return Err(PeachDynError::DomainAlreadyExistsError(full_domain)); + } + + // generate string with text for TSIG key file + let key_file_text = generate_tsig_key(&full_domain).expect("failed to generate tsig key"); + + // append key_file_text to /etc/bind/dyn.commoninternet.net.keys + let key_file_path = "/etc/bind/dyn.commoninternet.net.keys"; + let mut file = OpenOptions::new().append(true).open(key_file_path) + .expect(&format!("failed to open {}", key_file_path)); + if let Err(e) = writeln!(file, "{}", key_file_text) { + eprintln!("Couldn't write to file: {}", e); + } + + // append zone section to /etc/bind/named.conf.local + let bind_conf_path = "/etc/bind/named.conf.local"; + let mut file = OpenOptions::new() + .append(true) + .open(bind_conf_path) + .expect(&format!("failed to open {}", bind_conf_path)); + let zone_section_text = format!( + "\ + zone \"{full_domain}\" {{ + type master; + file \"/var/lib/bind/{full_domain}\"; + update-policy {{ + grant {full_domain} self {full_domain}; + }}; + }}; + ", + full_domain = full_domain + ); + if let Err(e) = writeln!(file, "{}", zone_section_text) { + eprintln!("Couldn't write to file: {}", e); + } + + // use tera to render the zone file + let tera = match Tera::new("templates/*.tera") { + Ok(t) => t, + Err(e) => { + println!("Parsing error(s): {}", e); + ::std::process::exit(1); + } + }; + let mut context = Context::new(); + context.insert("full_domain", &full_domain); + let result = tera.render("zonefile.tera", &context).expect("error loading zonefile.tera"); + + // write new zone file to /var/lib/bind + let zone_file_path = format!("/var/lib/bind/{}", full_domain); + let mut file = File::create(&zone_file_path) + .expect(&format!("failed to create {}", zone_file_path)); + if let Err(e) = writeln!(file, "{}", result) { + eprintln!("Couldn't write to file: {}", e) + }; + + // restart bind + // we use the /etc/sudoers.d/bindctl to allow peach-dyndns user to restart bind as sudo without entering a password + // using a binary at /bin/reloadbind which runs 'systemctl reload bind9' + let status = Command::new("sudo") + .arg("/bin/reloadbind") + .status().expect("error restarting bind9"); + if !status.success() { + return Err(PeachDynError::BindConfigurationError("There was an error in the bind configuration".to_string())); + // TODO: for extra safety consider to revert bind configurations to whatever they were before + } + + // return success + Ok(key_file_text) +} + +// this main function is only used for the purpose of testing +fn main() { + + let result = generate_zone("purple"); + match result { + Ok(key_text) => { + println!("successfully created zone"); + println!("{}", key_text); + } + Err(err) => { + println!("error creating zone {:?}", err); + } + } +} diff --git a/src/http.rs b/src/http.rs index c258be9..4df4098 100644 --- a/src/http.rs +++ b/src/http.rs @@ -6,10 +6,10 @@ * /update-domain (update the IP for the domain, passing the associated secret) * */ -use rocket_contrib::json::Json; +use crate::generate_zone::{check_domain_available, generate_zone}; +use rocket_contrib::json::{Json, JsonValue}; use serde::Deserialize; use std::thread; -use crate::client::check_domain_available; #[get("/")] pub fn index() -> &'static str { @@ -25,15 +25,19 @@ pub struct RegisterDomainPost { pub async fn register_domain(data: Json) -> &'static str { info!("++ post request to register new domain: {:?}", data); // TODO: first confirm domain is in the right format ("*.dyn.peachcloud.org") - let handle = thread::spawn(move || { - let domain_already_exists = check_domain_available(&data.domain); - domain_already_exists - }); - let domain_already_exists = handle.join().unwrap(); - if domain_already_exists { - "can't register domain already exists" + let is_domain_available = check_domain_available(&data.domain); + if !is_domain_available{ + "can't register domain that already exists" } else { - // TODO: use bash to generate a tsig key, update bind config, and then return the secret - "New domain registered" + let result = generate_zone(&data.domain); + match result { + Ok(key_file_text) => { + // TODO: figure out how to return key_file_text + "successfully created zone" + } + Err(err) => { + "there was an error registering the domain" + } + } } -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index f1e681a..a41c2e8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,19 +3,19 @@ #[macro_use] extern crate rocket; +use crate::http::{index, register_domain}; use futures::try_join; use std::io; use tokio::task; -use crate::http::{index, register_domain}; mod cli; -mod http; mod client; +mod http; +mod generate_zone; #[tokio::main] async fn main() { - - let rocket_result= rocket::build() + let rocket_result = rocket::build() .mount("/", routes![index, register_domain]) .launch() .await; @@ -23,5 +23,4 @@ async fn main() { if let Err(err) = rocket_result { error!("++ error launching rocket server: {:?}", err); } - } diff --git a/src/utils.rs b/src/utils.rs deleted file mode 100644 index afed411..0000000 --- a/src/utils.rs +++ /dev/null @@ -1,93 +0,0 @@ -/* For each subdomain, -- generate a new ddns key (tsig-keygen -a hmac-md5 {{subdomain}}.dyn.commoninternet.net) and append it to /etc/bind/dyn.commoninternet.net.keys -- add a zone section to /etc/bind/named.conf.local, associating the key with the subdomain -- add a minimal zone file to /var/lib/bind/subdomain.dyn.commoninternet.net -- reload bind and return the secret key to the client -*/ -use std::process::Command; -use std::io::Error; -use std::io::Write; -use std::string::FromUtf8Error; -use std::{fs::OpenOptions}; -use std::fs::File; - -const BASE_DOMAIN : &str = "dyn.commoninternet.net"; - - -#[derive(Debug)] -pub enum PeachDynError { - GenerateTsigIoError(std::io::Error), - GenerateTsigParseError(std::string::FromUtf8Error), -} - -impl From for PeachDynError { - fn from(err: std::io::Error) -> PeachDynError { - PeachDynError::GenerateTsigIoError(err) - } -} - -impl From for PeachDynError { - fn from(err: std::string::FromUtf8Error) -> PeachDynError { - PeachDynError::GenerateTsigParseError(err) - } -} - - -/// helper function to generate a TSIG key file -pub fn generate_tsig_key(full_domain: &str) -> Result { - let output = Command::new("/usr/sbin/tsig-keygen") - .arg("-a") - .arg("hmac-md5") - .arg(full_domain) - .output()?; - let key_file_text = String::from_utf8(output.stdout)?; - Ok(key_file_text) -} - - - -fn generate_zone(subdomain: &str) { - - let full_domain=format!("{}.{}", subdomain, BASE_DOMAIN); - println!("[generating zone for {}]", subdomain); - - // generate key_file_text - let key_file_text = generate_tsig_key(&full_domain).unwrap(); - println!("key_file_text: {}", key_file_text); - - // write key_file_text to file - let key_file_path = "/etc/bind/dyn.commoninternet.net.keys"; - let mut file = OpenOptions::new() - .append(true) - .open(key_file_path).unwrap(); - if let Err(e) = writeln!(file, "{}", key_file_text) { - eprintln!("Couldn't write to file: {}", e); - } - - // append to named.local.conf - let bind_conf_path = "/etc/bind/named.local.conf"; - let mut file = OpenOptions::new() - .append(true) - .open(bind_conf_path).unwrap(); - let zone_section_text = format!("\ - zone \"{full_domain}\" {{ - type master; - file \"/var/lib/bind/{full_domain}\"; - update-policy {{ - grant {full_domain} self {full_domain}; - }}; - }}; - ", full_domain=full_domain); - if let Err(e) = writeln!(file, "{}", zone_section_text) { - eprintln!("Couldn't write to file: {}", e); - } - - - -} - - - -fn main() { - generate_zone("blue"); -} diff --git a/templates/zonefile.tera b/templates/zonefile.tera new file mode 100644 index 0000000..ef7fb9b --- /dev/null +++ b/templates/zonefile.tera @@ -0,0 +1,10 @@ +$ORIGIN . +$TTL 30 ; 30 seconds +{{full_domain}} IN SOA ns.commoninternet.net. root.commoninternet.net. ( + 2016062804 ; serial + 3600 ; refresh (1 hour) + 600 ; retry (10 minutes) + 2600 ; expire (43 minutes 20 seconds) + 30 ; minimum (30 seconds) + ) + NS ns.commoninternet.net. \ No newline at end of file From 7c626c5ef7b5bc90450cbd04c7c794e3a8554812 Mon Sep 17 00:00:00 2001 From: notplants Date: Mon, 17 May 2021 20:30:44 +0200 Subject: [PATCH 14/18] Fix small bug --- src/generate_zone.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/generate_zone.rs b/src/generate_zone.rs index 19de29d..14aed36 100644 --- a/src/generate_zone.rs +++ b/src/generate_zone.rs @@ -75,20 +75,18 @@ pub fn check_domain_available(full_domain: &str) -> bool { /// subdomain using dynamic DNS authenticated via a new TSIG key which is unique to that subdomain /// and thus only the possessor of that key can use nsupdate to modify the records /// for that subodmain -pub fn generate_zone(subdomain: &str) -> Result { +pub fn generate_zone(full_domain: &str) -> Result { - // construct full_domain - let full_domain = format!("{}.{}", subdomain, BASE_DOMAIN); - println!("[generating zone for {}]", full_domain); + // TODO: confirm that domain matches correct format // first safety check if the domain is available - let is_available = check_domain_available(&full_domain); + let is_available = check_domain_available(full_domain); if !is_available { - return Err(PeachDynError::DomainAlreadyExistsError(full_domain)); + return Err(PeachDynError::DomainAlreadyExistsError(full_domain.to_string())); } // generate string with text for TSIG key file - let key_file_text = generate_tsig_key(&full_domain).expect("failed to generate tsig key"); + let key_file_text = generate_tsig_key(full_domain).expect("failed to generate tsig key"); // append key_file_text to /etc/bind/dyn.commoninternet.net.keys let key_file_path = "/etc/bind/dyn.commoninternet.net.keys"; @@ -129,7 +127,7 @@ pub fn generate_zone(subdomain: &str) -> Result { } }; let mut context = Context::new(); - context.insert("full_domain", &full_domain); + context.insert("full_domain", full_domain); let result = tera.render("zonefile.tera", &context).expect("error loading zonefile.tera"); // write new zone file to /var/lib/bind From 4d14a58ff78c00284d81cee8180dc84486df9c74 Mon Sep 17 00:00:00 2001 From: notplants Date: Tue, 18 May 2021 09:25:18 +0200 Subject: [PATCH 15/18] Working json response with keyfile --- .gitignore | 1 + Cargo.toml | 9 ------- src/client.rs | 57 --------------------------------------- src/constants.rs | 5 ++++ src/errors.rs | 22 ++++++++++++++++ src/generate_zone.rs | 50 ++++++----------------------------- src/http.rs | 43 ------------------------------ src/main.rs | 7 ++--- src/routes.rs | 63 ++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 103 insertions(+), 154 deletions(-) delete mode 100644 src/client.rs create mode 100644 src/constants.rs create mode 100644 src/errors.rs delete mode 100644 src/http.rs create mode 100644 src/routes.rs diff --git a/.gitignore b/.gitignore index 7bd65eb..63340e3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target **/*.rs.bk ns_tests/*.key +ns_tests/* diff --git a/Cargo.toml b/Cargo.toml index 5df6d36..bf5e96f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,15 +23,6 @@ serde = "1.0.125" dotenv = "0.15.0" tera = "1" - -[[bin]] -name = "client" -path = "src/client.rs" - -[[bin]] -name = "zone" -path = "src/generate_zone.rs" - [[bin]] name = "main" path = "src/main.rs" \ No newline at end of file diff --git a/src/client.rs b/src/client.rs deleted file mode 100644 index 28612d3..0000000 --- a/src/client.rs +++ /dev/null @@ -1,57 +0,0 @@ -use futures::try_join; -use std::io; -use tokio::task; - -use std::net::Ipv4Addr; -use std::str::FromStr; -use trust_dns_client::client::{Client, SyncClient}; -use trust_dns_client::op::update_message; -use trust_dns_client::op::DnsResponse; -use trust_dns_client::rr::{DNSClass, Name, RData, Record, RecordSet, RecordType}; -use trust_dns_client::tcp::TcpClientConnection; -use trust_dns_client::udp::UdpClientConnection; -use trust_dns_server::authority::{ - AuthLookup, Authority, LookupError, MessageRequest, UpdateResult, -}; - -pub fn check_domain_available_using_nslookup(domain: &str) -> bool { - let address = "167.99.136.83:53".parse().unwrap(); - let conn = UdpClientConnection::new(address).unwrap(); - let client = SyncClient::new(conn); - let name = Name::from_str(domain).unwrap(); - - println!("++ making query {:?}", domain); - let response: DnsResponse = client.query(&name, DNSClass::IN, RecordType::A).unwrap(); - println!("++ received response"); - let answers: &[Record] = response.answers(); - - if answers.len() > 0 { - println!("found: {:?}", answers[0].rdata()); - true - } else { - false - } -} - -fn update_test() { - let address = "167.99.136.83:53".parse().unwrap(); - let conn = UdpClientConnection::new(address).unwrap(); - let client = SyncClient::new(conn); - - // Specify the name, note the final '.' which specifies it's an FQDN - let name = Name::from_str("test.time.commoninternet.net").unwrap(); - - let record = Record::from_rdata(name.clone(), 8, RData::A(Ipv4Addr::new(127, 0, 0, 10))); - let rrset: RecordSet = record.clone().into(); - let zone_origin = Name::from_str("time.commoninternet.net").unwrap(); - - let response: DnsResponse = client - .create(rrset, zone_origin) - .expect("failed to create record"); - println!("response: {:?}", response); -} - -fn main() { - check_domain_available_using_nslookup("test"); - // update_test(); -} diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..f7b62f9 --- /dev/null +++ b/src/constants.rs @@ -0,0 +1,5 @@ + + +// this is the base domain which peach-dyndns creates subdomains under +// e.g. blue.dyn.peachcloud.org +pub const BASE_DOMAIN: &str = "dyn.commoninternet.net"; \ No newline at end of file diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..fdb3808 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,22 @@ +use std::string::FromUtf8Error; + + +#[derive(Debug)] +pub enum PeachDynError { + GenerateTsigIoError(std::io::Error), + GenerateTsigParseError(std::string::FromUtf8Error), + DomainAlreadyExistsError(String), + BindConfigurationError(String), +} + +impl From for PeachDynError { + fn from(err: std::io::Error) -> PeachDynError { + PeachDynError::GenerateTsigIoError(err) + } +} + +impl From for PeachDynError { + fn from(err: std::string::FromUtf8Error) -> PeachDynError { + PeachDynError::GenerateTsigParseError(err) + } +} \ No newline at end of file diff --git a/src/generate_zone.rs b/src/generate_zone.rs index 14aed36..c9436e6 100644 --- a/src/generate_zone.rs +++ b/src/generate_zone.rs @@ -8,33 +8,12 @@ use std::fs::File; use std::fs::OpenOptions; use std::io::Write; use std::process::Command; -use std::string::FromUtf8Error; use tera::{Tera, Context}; +use crate::errors::PeachDynError; +use crate::constants::BASE_DOMAIN; -const BASE_DOMAIN: &str = "dyn.commoninternet.net"; - -#[derive(Debug)] -pub enum PeachDynError { - GenerateTsigIoError(std::io::Error), - GenerateTsigParseError(std::string::FromUtf8Error), - DomainAlreadyExistsError(String), - BindConfigurationError(String), -} - -impl From for PeachDynError { - fn from(err: std::io::Error) -> PeachDynError { - PeachDynError::GenerateTsigIoError(err) - } -} - -impl From for PeachDynError { - fn from(err: std::string::FromUtf8Error) -> PeachDynError { - PeachDynError::GenerateTsigParseError(err) - } -} - -/// helper function to generate the text of a TSIG key file +/// function to generate the text of a TSIG key file pub fn generate_tsig_key(full_domain: &str) -> Result { let output = Command::new("/usr/sbin/tsig-keygen") .arg("-a") @@ -45,7 +24,8 @@ pub fn generate_tsig_key(full_domain: &str) -> Result { Ok(key_file_text) } -/// helper function which returns true if all three conditions are true +/// function which helps us guarantee that a given domain is not already being used by bind +/// it checks three places for the domain, and only returns true if it is not found in all three places /// - no already extant tsig key for the given domain /// - no zone file for the given domain in /var/lib/bind /// - no zone section for the given domain in named.conf.local @@ -64,6 +44,7 @@ pub fn check_domain_available(full_domain: &str) -> bool { // domain is only available if domain does not exist in either named.conf.local or dyn.commoninternet.netkeys // and a file with that name is not found in /var/lib/bind/ + // grep returns a status code of 1 if lines are not found, which is why we check that the codes equal 1 let domain_available = (code1 == 1) & (code2 == 1) & (!condition3); // return @@ -71,9 +52,9 @@ pub fn check_domain_available(full_domain: &str) -> bool { } -/// helper function which generates all necessary bind configuration to serve the given +/// function which generates all necessary bind configuration to serve the given /// subdomain using dynamic DNS authenticated via a new TSIG key which is unique to that subdomain -/// and thus only the possessor of that key can use nsupdate to modify the records +/// - thus only the possessor of that key can use nsupdate to modify the records /// for that subodmain pub fn generate_zone(full_domain: &str) -> Result { @@ -152,18 +133,3 @@ pub fn generate_zone(full_domain: &str) -> Result { // return success Ok(key_file_text) } - -// this main function is only used for the purpose of testing -fn main() { - - let result = generate_zone("purple"); - match result { - Ok(key_text) => { - println!("successfully created zone"); - println!("{}", key_text); - } - Err(err) => { - println!("error creating zone {:?}", err); - } - } -} diff --git a/src/http.rs b/src/http.rs deleted file mode 100644 index 4df4098..0000000 --- a/src/http.rs +++ /dev/null @@ -1,43 +0,0 @@ -/* -* -* /register-user (sends an email verification to create a new account) -* /verify (for clicking the link in the email) -* /register-domain (add a new domain and get back the secret for subsequent updating) -* /update-domain (update the IP for the domain, passing the associated secret) -* -*/ -use crate::generate_zone::{check_domain_available, generate_zone}; -use rocket_contrib::json::{Json, JsonValue}; -use serde::Deserialize; -use std::thread; - -#[get("/")] -pub fn index() -> &'static str { - "This is the peach-dyn-dns server." -} - -#[derive(Deserialize, Debug)] -pub struct RegisterDomainPost { - domain: String, -} - -#[post("/register-domain", data = "")] -pub async fn register_domain(data: Json) -> &'static str { - info!("++ post request to register new domain: {:?}", data); - // TODO: first confirm domain is in the right format ("*.dyn.peachcloud.org") - let is_domain_available = check_domain_available(&data.domain); - if !is_domain_available{ - "can't register domain that already exists" - } else { - let result = generate_zone(&data.domain); - match result { - Ok(key_file_text) => { - // TODO: figure out how to return key_file_text - "successfully created zone" - } - Err(err) => { - "there was an error registering the domain" - } - } - } -} diff --git a/src/main.rs b/src/main.rs index a41c2e8..26fab5c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,14 +3,15 @@ #[macro_use] extern crate rocket; -use crate::http::{index, register_domain}; +use crate::routes::{index, register_domain}; use futures::try_join; use std::io; use tokio::task; mod cli; -mod client; -mod http; +mod routes; +mod errors; +mod constants; mod generate_zone; #[tokio::main] diff --git a/src/routes.rs b/src/routes.rs new file mode 100644 index 0000000..5dd5bcb --- /dev/null +++ b/src/routes.rs @@ -0,0 +1,63 @@ +/* +* +* /register-user (sends an email verification to create a new account) NOT IMPLEMENTED +* /verify (for clicking the link in the email) NOT IMPLEMENTED +* /register-domain (add a new domain and get back the TSIG key for subsequent updating with nsupdate) +*/ +use crate::generate_zone::{check_domain_available, generate_zone}; +use rocket_contrib::json::{Json, JsonValue}; +use serde::{Deserialize, Serialize}; + +#[get("/")] +pub fn index() -> &'static str { + "This is the peach-dyndns server." +} + +#[derive(Serialize)] +pub struct JsonResponse { + pub status: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub msg: Option, +} + +// helper function to build a JsonResponse object +pub fn build_json_response( + status: String, + data: Option, + msg: Option, +) -> JsonResponse { + JsonResponse { status, data, msg } +} + +#[derive(Deserialize, Debug)] +pub struct RegisterDomainPost { + domain: String, +} + +#[post("/register-domain", data = "")] +pub async fn register_domain(data: Json) -> Json { + info!("++ post request to register new domain: {:?}", data); + // TODO: first confirm domain is in the right format ("*.dyn.peachcloud.org") + let is_domain_available = check_domain_available(&data.domain); + if !is_domain_available{ + let status = "error".to_string(); + let msg = "can't register a domain that is already registered".to_string(); + Json(build_json_response(status, None, Some(msg))) + } else { + let result = generate_zone(&data.domain); + match result { + Ok(key_file_text) => { + let status = "success".to_string(); + let msg = key_file_text.to_string(); + Json(build_json_response(status, None, Some(msg))) + } + Err(_err) => { + let status = "error".to_string(); + let msg = "there was an error creating the zone file".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } + } +} From c2431b6225e5fcb4ece38117cb02f46f4ac6d107 Mon Sep 17 00:00:00 2001 From: notplants Date: Tue, 18 May 2021 14:20:17 +0200 Subject: [PATCH 16/18] Add /domain/check-available endpoint --- src/main.rs | 5 ++--- src/routes.rs | 28 +++++++++++++++++++++++----- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/main.rs b/src/main.rs index 26fab5c..5ece8de 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,8 +3,7 @@ #[macro_use] extern crate rocket; -use crate::routes::{index, register_domain}; -use futures::try_join; +use crate::routes::{index, register_domain, check_available}; use std::io; use tokio::task; @@ -17,7 +16,7 @@ mod generate_zone; #[tokio::main] async fn main() { let rocket_result = rocket::build() - .mount("/", routes![index, register_domain]) + .mount("/", routes![index, register_domain, check_available]) .launch() .await; diff --git a/src/routes.rs b/src/routes.rs index 5dd5bcb..d94e2c4 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -1,8 +1,9 @@ /* -* -* /register-user (sends an email verification to create a new account) NOT IMPLEMENTED -* /verify (for clicking the link in the email) NOT IMPLEMENTED -* /register-domain (add a new domain and get back the TSIG key for subsequent updating with nsupdate) +* LIST OF ROUTES +* /domain/register (add a new domain and get back the TSIG key for subsequent updating with nsupdate) +* /domain/check-available (check if given domain is available) +* /user/register sends an email verification to create a new account) NOT IMPLEMENTED +* /user/verify (for clicking the link in the email) NOT IMPLEMENTED */ use crate::generate_zone::{check_domain_available, generate_zone}; use rocket_contrib::json::{Json, JsonValue}; @@ -36,9 +37,10 @@ pub struct RegisterDomainPost { domain: String, } -#[post("/register-domain", data = "")] +#[post("/domain/register", data = "")] pub async fn register_domain(data: Json) -> Json { info!("++ post request to register new domain: {:?}", data); + // TODO: grab/create a mutex, so that only one rocket thread is calling register_domain at a time // TODO: first confirm domain is in the right format ("*.dyn.peachcloud.org") let is_domain_available = check_domain_available(&data.domain); if !is_domain_available{ @@ -61,3 +63,19 @@ pub async fn register_domain(data: Json) -> Json) -> Json { + info!("post request to check if domain is available {:?}", data); + // TODO: validate that domain is in correct format + let status = "success".to_string(); + let is_available = check_domain_available(&data.domain); + let msg = is_available.to_string(); + Json(build_json_response(status, None, Some(msg))) +} From b6ecf6eda39cea36b54083994b6f7e5a6b18c584 Mon Sep 17 00:00:00 2001 From: notplants Date: Tue, 18 May 2021 18:49:01 +0200 Subject: [PATCH 17/18] Add domain validation and cleanup --- Cargo.lock | 1 + Cargo.toml | 1 + src/constants.rs | 5 ++-- src/errors.rs | 1 + src/generate_zone.rs | 42 +++++++++++++++++++-------------- src/main.rs | 4 ++-- src/routes.rs | 55 +++++++++++++++++++++++++++----------------- 7 files changed, 66 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index acd3f31..6b47eee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1308,6 +1308,7 @@ dependencies = [ "futures 0.3.14", "log", "nest", + "regex", "rocket", "rocket_contrib", "serde 1.0.125", diff --git a/Cargo.toml b/Cargo.toml index bf5e96f..772d13d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ rocket_contrib = { git = "https://github.com/SergioBenitez/Rocket", branch = "ma serde = "1.0.125" dotenv = "0.15.0" tera = "1" +regex = "1" [[bin]] name = "main" diff --git a/src/constants.rs b/src/constants.rs index f7b62f9..c77125a 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,5 +1,4 @@ - -// this is the base domain which peach-dyndns creates subdomains under +// this regex is used to validate that domains are in the correct format // e.g. blue.dyn.peachcloud.org -pub const BASE_DOMAIN: &str = "dyn.commoninternet.net"; \ No newline at end of file +pub const DOMAIN_REGEX: &str = r"^.*\.dyn\.commointernet\.net$"; \ No newline at end of file diff --git a/src/errors.rs b/src/errors.rs index fdb3808..d3ab7fc 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -7,6 +7,7 @@ pub enum PeachDynError { GenerateTsigParseError(std::string::FromUtf8Error), DomainAlreadyExistsError(String), BindConfigurationError(String), + InvalidDomainError(String) } impl From for PeachDynError { diff --git a/src/generate_zone.rs b/src/generate_zone.rs index c9436e6..a5108d5 100644 --- a/src/generate_zone.rs +++ b/src/generate_zone.rs @@ -1,8 +1,6 @@ -/* For each subdomain, -- generate a new ddns key (tsig-keygen -a hmac-md5 {{subdomain}}.dyn.commoninternet.net) and append it to /etc/bind/dyn.commoninternet.net.keys -- add a zone section to /etc/bind/named.conf.local, associating the key with the subdomain -- add a minimal zone file to /var/lib/bind/subdomain.dyn.commoninternet.net -- reload bind and return the secret key to the client +/* +* Functions for generating bind9 configurations to enable dynamic dns for a subdomain via TSIG authentication +* which is unique to that subdomain */ use std::fs::File; use std::fs::OpenOptions; @@ -10,7 +8,7 @@ use std::io::Write; use std::process::Command; use tera::{Tera, Context}; use crate::errors::PeachDynError; -use crate::constants::BASE_DOMAIN; +use crate::constants::DOMAIN_REGEX; /// function to generate the text of a TSIG key file @@ -24,6 +22,13 @@ pub fn generate_tsig_key(full_domain: &str) -> Result { Ok(key_file_text) } +// function returns true if domain is of the format something.dyn.peachcloud.org +pub fn validate_domain(domain: &str) -> bool { + use regex::Regex; + let re = Regex::new(DOMAIN_REGEX).unwrap(); + re.is_match(domain) +} + /// function which helps us guarantee that a given domain is not already being used by bind /// it checks three places for the domain, and only returns true if it is not found in all three places /// - no already extant tsig key for the given domain @@ -54,13 +59,20 @@ pub fn check_domain_available(full_domain: &str) -> bool { /// function which generates all necessary bind configuration to serve the given /// subdomain using dynamic DNS authenticated via a new TSIG key which is unique to that subdomain -/// - thus only the possessor of that key can use nsupdate to modify the records +/// thus only the possessor of that key can use nsupdate to modify the records /// for that subodmain +/// - generate a new ddns key (tsig-keygen -a hmac-md5 {{subdomain}}.dyn.commoninternet.net) and append it to /etc/bind/dyn.commoninternet.net.keys +/// - add a zone section to /etc/bind/named.conf.local, associating the key with the subdomain +/// - add a minimal zone file to /var/lib/bind/subdomain.dyn.commoninternet.net +/// - reload bind and return the secret key to the client pub fn generate_zone(full_domain: &str) -> Result { - // TODO: confirm that domain matches correct format + // first safety check domain is in correct format + if !validate_domain(full_domain) { + return Err(PeachDynError::InvalidDomainError(full_domain.to_string())); + } - // first safety check if the domain is available + // safety check if the domain is available let is_available = check_domain_available(full_domain); if !is_available { return Err(PeachDynError::DomainAlreadyExistsError(full_domain.to_string())); @@ -74,7 +86,7 @@ pub fn generate_zone(full_domain: &str) -> Result { let mut file = OpenOptions::new().append(true).open(key_file_path) .expect(&format!("failed to open {}", key_file_path)); if let Err(e) = writeln!(file, "{}", key_file_text) { - eprintln!("Couldn't write to file: {}", e); + error!("Couldn't write to file: {}", e); } // append zone section to /etc/bind/named.conf.local @@ -95,15 +107,13 @@ pub fn generate_zone(full_domain: &str) -> Result { ", full_domain = full_domain ); - if let Err(e) = writeln!(file, "{}", zone_section_text) { - eprintln!("Couldn't write to file: {}", e); - } + writeln!(file, "{}", zone_section_text).expect(&format!("Couldn't write to file: {}", bind_conf_path)); // use tera to render the zone file let tera = match Tera::new("templates/*.tera") { Ok(t) => t, Err(e) => { - println!("Parsing error(s): {}", e); + info!("Parsing error(s): {}", e); ::std::process::exit(1); } }; @@ -115,9 +125,7 @@ pub fn generate_zone(full_domain: &str) -> Result { let zone_file_path = format!("/var/lib/bind/{}", full_domain); let mut file = File::create(&zone_file_path) .expect(&format!("failed to create {}", zone_file_path)); - if let Err(e) = writeln!(file, "{}", result) { - eprintln!("Couldn't write to file: {}", e) - }; + writeln!(file, "{}", result).expect(&format!("Couldn't write to file: {}", zone_file_path)); // restart bind // we use the /etc/sudoers.d/bindctl to allow peach-dyndns user to restart bind as sudo without entering a password diff --git a/src/main.rs b/src/main.rs index 5ece8de..34a8770 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,8 +4,6 @@ extern crate rocket; use crate::routes::{index, register_domain, check_available}; -use std::io; -use tokio::task; mod cli; mod routes; @@ -15,6 +13,8 @@ mod generate_zone; #[tokio::main] async fn main() { + let _args = cli::args().expect("error parsing args"); + let rocket_result = rocket::build() .mount("/", routes![index, register_domain, check_available]) .launch() diff --git a/src/routes.rs b/src/routes.rs index d94e2c4..8264dd3 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -5,7 +5,7 @@ * /user/register sends an email verification to create a new account) NOT IMPLEMENTED * /user/verify (for clicking the link in the email) NOT IMPLEMENTED */ -use crate::generate_zone::{check_domain_available, generate_zone}; +use crate::generate_zone::{check_domain_available, generate_zone, validate_domain}; use rocket_contrib::json::{Json, JsonValue}; use serde::{Deserialize, Serialize}; @@ -41,24 +41,32 @@ pub struct RegisterDomainPost { pub async fn register_domain(data: Json) -> Json { info!("++ post request to register new domain: {:?}", data); // TODO: grab/create a mutex, so that only one rocket thread is calling register_domain at a time - // TODO: first confirm domain is in the right format ("*.dyn.peachcloud.org") - let is_domain_available = check_domain_available(&data.domain); - if !is_domain_available{ + // check if its a valid domain + if !validate_domain(&data.domain) { let status = "error".to_string(); - let msg = "can't register a domain that is already registered".to_string(); + let msg = "domain is not in a valid format".to_string(); Json(build_json_response(status, None, Some(msg))) } else { - let result = generate_zone(&data.domain); - match result { - Ok(key_file_text) => { - let status = "success".to_string(); - let msg = key_file_text.to_string(); - Json(build_json_response(status, None, Some(msg))) - } - Err(_err) => { - let status = "error".to_string(); - let msg = "there was an error creating the zone file".to_string(); - Json(build_json_response(status, None, Some(msg))) + // check if the domain is available + let is_domain_available = check_domain_available(&data.domain); + if !is_domain_available { + let status = "error".to_string(); + let msg = "can't register a domain that is already registered".to_string(); + Json(build_json_response(status, None, Some(msg))) + } else { + // generate configs for the zone + let result = generate_zone(&data.domain); + match result { + Ok(key_file_text) => { + let status = "success".to_string(); + let msg = key_file_text.to_string(); + Json(build_json_response(status, None, Some(msg))) + } + Err(_err) => { + let status = "error".to_string(); + let msg = "there was an error creating the zone file".to_string(); + Json(build_json_response(status, None, Some(msg))) + } } } } @@ -73,9 +81,14 @@ pub struct CheckAvailableDomainPost { #[post("/domain/check-available", data = "")] pub async fn check_available(data: Json) -> Json { info!("post request to check if domain is available {:?}", data); - // TODO: validate that domain is in correct format - let status = "success".to_string(); - let is_available = check_domain_available(&data.domain); - let msg = is_available.to_string(); - Json(build_json_response(status, None, Some(msg))) + if !validate_domain(&data.domain) { + let status = "error".to_string(); + let msg = "domain is not in a valid format".to_string(); + Json(build_json_response(status, None, Some(msg))) + } else { + let status = "success".to_string(); + let is_available = check_domain_available(&data.domain); + let msg = is_available.to_string(); + Json(build_json_response(status, None, Some(msg))) + } } From 34f98de61d1e7acd99f15f338441f305a79e7cab Mon Sep 17 00:00:00 2001 From: notplants Date: Tue, 18 May 2021 18:53:52 +0200 Subject: [PATCH 18/18] Fix regex --- src/constants.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants.rs b/src/constants.rs index c77125a..174409a 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,4 +1,4 @@ // this regex is used to validate that domains are in the correct format // e.g. blue.dyn.peachcloud.org -pub const DOMAIN_REGEX: &str = r"^.*\.dyn\.commointernet\.net$"; \ No newline at end of file +pub const DOMAIN_REGEX: &str = r"^.*\.dyn\.commoninternet\.net$"; \ No newline at end of file