]> git.proxmox.com Git - proxmox.git/commitdiff
dns-api: new crate which implements the DNS api
authorDietmar Maurer <dietmar@proxmox.com>
Thu, 2 May 2024 11:04:43 +0000 (13:04 +0200)
committerDietmar Maurer <dietmar@proxmox.com>
Thu, 2 May 2024 11:04:43 +0000 (13:04 +0200)
Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
Cargo.toml
proxmox-dns-api/Cargo.toml [new file with mode: 0644]
proxmox-dns-api/src/api_types.rs [new file with mode: 0644]
proxmox-dns-api/src/lib.rs [new file with mode: 0644]

index 2780d424459c87e34e0ffd7467530cbf7d240969..f07f35d5fdcb087458cbe667d6aede6ace701ebc 100644 (file)
@@ -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 (file)
index 0000000..8021219
--- /dev/null
@@ -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 (file)
index 0000000..fb63771
--- /dev/null
@@ -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<String>,
+    pub dns1: Option<String>,
+    pub dns2: Option<String>,
+    pub dns3: Option<String>,
+    pub options: Option<String>,
+}
+
+#[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 (file)
index 0000000..6d0091a
--- /dev/null
@@ -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<ResolvConfWithDigest, Error> {
+    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<Vec<DeletableResolvConfProperty>>,
+    digest: Option<ConfigDigest>,
+) -> Result<(), Error> {
+    lazy_static! {
+        static ref MUTEX: Arc<Mutex<()>> = 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(())
+}