From b7f0cc7c1eaba563d5704ae716616e53fd901641 Mon Sep 17 00:00:00 2001 From: Dietmar Maurer Date: Thu, 30 May 2024 08:43:32 +0200 Subject: [PATCH] dns-api: new crate, split out from system-management-api Signed-off-by: Dietmar Maurer --- Cargo.toml | 1 + proxmox-dns-api/Cargo.toml | 31 ++++++ proxmox-dns-api/debian/changelog | 5 + proxmox-dns-api/debian/copyright | 18 ++++ proxmox-dns-api/debian/debcargo.toml | 7 ++ proxmox-dns-api/src/api_types.rs | 94 +++++++++++++++++ proxmox-dns-api/src/lib.rs | 7 ++ proxmox-dns-api/src/resolv_conf.rs | 144 +++++++++++++++++++++++++++ 8 files changed, 307 insertions(+) create mode 100644 proxmox-dns-api/Cargo.toml create mode 100644 proxmox-dns-api/debian/changelog create mode 100644 proxmox-dns-api/debian/copyright create mode 100644 proxmox-dns-api/debian/debcargo.toml create mode 100644 proxmox-dns-api/src/api_types.rs create mode 100644 proxmox-dns-api/src/lib.rs create mode 100644 proxmox-dns-api/src/resolv_conf.rs diff --git a/Cargo.toml b/Cargo.toml index ba0f95a4..75d86b75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "proxmox-client", "proxmox-compression", "proxmox-config-digest", + "proxmox-dns-api", "proxmox-http", "proxmox-http-error", "proxmox-human-byte", diff --git a/proxmox-dns-api/Cargo.toml b/proxmox-dns-api/Cargo.toml new file mode 100644 index 00000000..bb067629 --- /dev/null +++ b/proxmox-dns-api/Cargo.toml @@ -0,0 +1,31 @@ +[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 Management API implementation" + +[dependencies] +anyhow.workspace = true +const_format.workspace = true +lazy_static.workspace = true +regex.workspace = true +serde = { workspace = true, features = ["derive"] } + +proxmox-config-digest.workspace = true +proxmox-schema = { workspace = true, features = ["api-macro", "api-types"] } + +proxmox-sys = { workspace = true, optional = true } +proxmox-time = { workspace = true, optional = true } +proxmox-product-config = { workspace = true, optional = true } + +[features] +default = [] +impl = [ + "proxmox-config-digest/openssl", + "dep:proxmox-sys", + "dep:proxmox-time", +] diff --git a/proxmox-dns-api/debian/changelog b/proxmox-dns-api/debian/changelog new file mode 100644 index 00000000..129123ec --- /dev/null +++ b/proxmox-dns-api/debian/changelog @@ -0,0 +1,5 @@ +rust-proxmox-dns-api (0.1.0-1) bookworm; urgency=medium + + * initial packaging (split out from proxmox-system-management-api) + + -- Proxmox Support Team Thu, 30 May 2024 08:14:29 +0200 diff --git a/proxmox-dns-api/debian/copyright b/proxmox-dns-api/debian/copyright new file mode 100644 index 00000000..0d9eab3e --- /dev/null +++ b/proxmox-dns-api/debian/copyright @@ -0,0 +1,18 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ + +Files: + * +Copyright: 2019 - 2023 Proxmox Server Solutions GmbH +License: AGPL-3.0-or-later + This program is free software: you can redistribute it and/or modify it under + the terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or (at your option) any + later version. + . + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + details. + . + You should have received a copy of the GNU Affero General Public License along + with this program. If not, see . diff --git a/proxmox-dns-api/debian/debcargo.toml b/proxmox-dns-api/debian/debcargo.toml new file mode 100644 index 00000000..b7864cdb --- /dev/null +++ b/proxmox-dns-api/debian/debcargo.toml @@ -0,0 +1,7 @@ +overlay = "." +crate_src_path = ".." +maintainer = "Proxmox Support Team " + +[source] +vcs_git = "git://git.proxmox.com/git/proxmox.git" +vcs_browser = "https://git.proxmox.com/?p=proxmox.git" diff --git a/proxmox-dns-api/src/api_types.rs b/proxmox-dns-api/src/api_types.rs new file mode 100644 index 00000000..f0ff70f4 --- /dev/null +++ b/proxmox-dns-api/src/api_types.rs @@ -0,0 +1,94 @@ +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_config_digest::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 { + #[serde(skip_serializing_if = "Option::is_none")] + pub search: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub dns1: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub dns2: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub dns3: Option, + #[serde(skip_serializing_if = "Option::is_none")] + 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..c6cf61cb --- /dev/null +++ b/proxmox-dns-api/src/lib.rs @@ -0,0 +1,7 @@ +mod api_types; +pub use api_types::*; + +#[cfg(feature = "impl")] +mod resolv_conf; +#[cfg(feature = "impl")] +pub use resolv_conf::*; diff --git a/proxmox-dns-api/src/resolv_conf.rs b/proxmox-dns-api/src/resolv_conf.rs new file mode 100644 index 00000000..1b09d07f --- /dev/null +++ b/proxmox-dns-api/src/resolv_conf.rs @@ -0,0 +1,144 @@ +use std::sync::Arc; +use std::sync::Mutex; + +use anyhow::Error; +use const_format::concatcp; +use lazy_static::lazy_static; +use proxmox_config_digest::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; + +use super::DeletableResolvConfProperty; +use super::ResolvConf; +use super::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<&ConfigDigest>, +) -> Result { + let mut config = ResolvConf::default(); + + let mut nscount = 0; + + let raw = file_get_contents(RESOLV_CONF_FN)?; + let digest = ConfigDigest::from_slice(&raw); + + digest.detect_modification(expected_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_ref())?; + + 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(()) +}