--- /dev/null
+[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
--- /dev/null
+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,
+}
--- /dev/null
+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(())
+}