From c2f85d418a919ffa8c49e74c06da568cc0a91f13 Mon Sep 17 00:00:00 2001 From: Dietmar Maurer Date: Thu, 2 May 2024 13:04:43 +0200 Subject: [PATCH] dns-api: new crate which implements the DNS api Signed-off-by: Dietmar Maurer --- Cargo.toml | 2 + proxmox-dns-api/Cargo.toml | 22 +++++ proxmox-dns-api/src/api_types.rs | 90 +++++++++++++++++++ proxmox-dns-api/src/lib.rs | 143 +++++++++++++++++++++++++++++++ 4 files changed, 257 insertions(+) create mode 100644 proxmox-dns-api/Cargo.toml create mode 100644 proxmox-dns-api/src/api_types.rs create mode 100644 proxmox-dns-api/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 2780d424..f07f35d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "proxmox-borrow", "proxmox-client", "proxmox-compression", + "proxmox-dns-api", "proxmox-http", "proxmox-http-error", "proxmox-human-byte", @@ -108,6 +109,7 @@ proxmox-human-byte = { version = "0.1.0", path = "proxmox-human-byte" } proxmox-io = { version = "1.0.0", path = "proxmox-io" } proxmox-lang = { version = "1.1", path = "proxmox-lang" } proxmox-login = { version = "0.1.0", path = "proxmox-login" } +proxmox-product-config = { vertsion = "0.1.0", path = "proxmox-product-config" } proxmox-rest-server = { version = "0.5.2", path = "proxmox-rest-server" } proxmox-router = { version = "2.1.3", path = "proxmox-router" } proxmox-schema = { version = "3.1.0", path = "proxmox-schema" } diff --git a/proxmox-dns-api/Cargo.toml b/proxmox-dns-api/Cargo.toml new file mode 100644 index 00000000..80212192 --- /dev/null +++ b/proxmox-dns-api/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "proxmox-dns-api" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +exclude.workspace = true +description = "DNS API implementation (read/write /etc/resolv.conf)" + +[dependencies] +anyhow.workspace = true +const_format.workspace = true +lazy_static.workspace = true +regex.workspace = true + +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } + +proxmox-sys.workspace = true +proxmox-schema = { workspace = true, features = ["api-macro", "api-types"] } +proxmox-product-config.workspace = true \ No newline at end of file diff --git a/proxmox-dns-api/src/api_types.rs b/proxmox-dns-api/src/api_types.rs new file mode 100644 index 00000000..fb637714 --- /dev/null +++ b/proxmox-dns-api/src/api_types.rs @@ -0,0 +1,90 @@ +use serde::{Deserialize, Serialize}; + +use proxmox_schema::api; +use proxmox_schema::api_types::IP_FORMAT; +use proxmox_schema::Schema; +use proxmox_schema::StringSchema; + +use proxmox_product_config::ConfigDigest; + +pub const SEARCH_DOMAIN_SCHEMA: Schema = + StringSchema::new("Search domain for host-name lookup.").schema(); + +pub const FIRST_DNS_SERVER_SCHEMA: Schema = StringSchema::new("First name server IP address.") + .format(&IP_FORMAT) + .schema(); + +pub const SECOND_DNS_SERVER_SCHEMA: Schema = StringSchema::new("Second name server IP address.") + .format(&IP_FORMAT) + .schema(); + +pub const THIRD_DNS_SERVER_SCHEMA: Schema = StringSchema::new("Third name server IP address.") + .format(&IP_FORMAT) + .schema(); + +#[api( + properties: { + search: { + schema: SEARCH_DOMAIN_SCHEMA, + optional: true, + }, + dns1: { + optional: true, + schema: FIRST_DNS_SERVER_SCHEMA, + }, + dns2: { + optional: true, + schema: SECOND_DNS_SERVER_SCHEMA, + }, + dns3: { + optional: true, + schema: THIRD_DNS_SERVER_SCHEMA, + }, + options: { + description: "Other data found in the configuration file (resolv.conf).", + optional: true, + }, + + } +)] +#[derive(Serialize, Deserialize, Default)] +/// DNS configuration from '/etc/resolv.conf' +pub struct ResolvConf { + pub search: Option, + pub dns1: Option, + pub dns2: Option, + pub dns3: Option, + pub options: Option, +} + +#[api( + properties: { + config: { + type: ResolvConf, + }, + digest: { + type: ConfigDigest, + }, + } +)] +#[derive(Serialize, Deserialize)] +/// DNS configuration with digest. +pub struct ResolvConfWithDigest { + #[serde(flatten)] + pub config: ResolvConf, + pub digest: ConfigDigest, +} + + +#[api()] +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +/// Deletable DNS configuration property name +pub enum DeletableResolvConfProperty { + /// Delete first nameserver entry + Dns1, + /// Delete second nameserver entry + Dns2, + /// Delete third nameserver entry + Dns3, +} diff --git a/proxmox-dns-api/src/lib.rs b/proxmox-dns-api/src/lib.rs new file mode 100644 index 00000000..6d0091ab --- /dev/null +++ b/proxmox-dns-api/src/lib.rs @@ -0,0 +1,143 @@ +use std::sync::Arc; +use std::sync::Mutex; + +use anyhow::Error; +use const_format::concatcp; +use lazy_static::lazy_static; +use proxmox_product_config::ConfigDigest; +use regex::Regex; + +use proxmox_sys::fs::file_get_contents; +use proxmox_sys::fs::replace_file; +use proxmox_sys::fs::CreateOptions; + +use proxmox_schema::api_types::IPRE_STR; + +mod api_types; +pub use api_types::{DeletableResolvConfProperty, ResolvConf, ResolvConfWithDigest}; + +static RESOLV_CONF_FN: &str = "/etc/resolv.conf"; + +/// Read DNS configuration from '/etc/resolv.conf'. +pub fn read_etc_resolv_conf( + expected_digest: Option<&[u8; 32]>, +) -> Result { + let mut config = ResolvConf::default(); + + let mut nscount = 0; + + let raw = file_get_contents(RESOLV_CONF_FN)?; + let digest = ConfigDigest::from_slice(&raw); + + proxmox_product_config::detect_modified_configuration_file(expected_digest, &digest)?; + + let data = String::from_utf8(raw)?; + + lazy_static! { + static ref DOMAIN_REGEX: Regex = Regex::new(r"^\s*(?:search|domain)\s+(\S+)\s*").unwrap(); + static ref SERVER_REGEX: Regex = + Regex::new(concatcp!(r"^\s*nameserver\s+(", IPRE_STR, r")\s*")).unwrap(); + } + + let mut options = String::new(); + + for line in data.lines() { + if let Some(caps) = DOMAIN_REGEX.captures(line) { + config.search = Some(caps[1].to_owned()); + } else if let Some(caps) = SERVER_REGEX.captures(line) { + nscount += 1; + if nscount > 3 { + continue; + }; + let nameserver = Some(caps[1].to_owned()); + match nscount { + 1 => config.dns1 = nameserver, + 2 => config.dns2 = nameserver, + 3 => config.dns3 = nameserver, + _ => continue, + } + } else { + if !options.is_empty() { + options.push('\n'); + } + options.push_str(line); + } + } + + if !options.is_empty() { + config.options = Some(options); + } + + Ok(ResolvConfWithDigest { config, digest }) +} + +/// Update DNS configuration, write result back to '/etc/resolv.conf'. +pub fn update_dns( + update: ResolvConf, + delete: Option>, + digest: Option, +) -> Result<(), Error> { + lazy_static! { + static ref MUTEX: Arc> = Arc::new(Mutex::new(())); + } + + let _guard = MUTEX.lock(); + + let ResolvConfWithDigest { mut config, .. } = read_etc_resolv_conf(digest.as_deref())?; + + if let Some(delete) = delete { + for delete_prop in delete { + match delete_prop { + DeletableResolvConfProperty::Dns1 => { + config.dns1 = None; + } + DeletableResolvConfProperty::Dns2 => { + config.dns2 = None; + } + DeletableResolvConfProperty::Dns3 => { + config.dns3 = None; + } + } + } + } + + if update.search.is_some() { + config.search = update.search; + } + if update.dns1.is_some() { + config.dns1 = update.dns1; + } + if update.dns2.is_some() { + config.dns2 = update.dns2; + } + if update.dns3.is_some() { + config.dns3 = update.dns3; + } + + let mut data = String::new(); + + use std::fmt::Write as _; + if let Some(search) = config.search { + let _ = writeln!(data, "search {}", search); + } + + if let Some(dns1) = config.dns1 { + let _ = writeln!(data, "nameserver {}", dns1); + } + + if let Some(dns2) = config.dns2 { + let _ = writeln!(data, "nameserver {}", dns2); + } + + if let Some(dns3) = config.dns3 { + let _ = writeln!(data, "nameserver {}", dns3); + } + + if let Some(options) = config.options { + data.push_str(&options); + } + + replace_file(RESOLV_CONF_FN, data.as_bytes(), CreateOptions::new(), true)?; + + Ok(()) +}