From: Aaron Lauterer <a.lauterer@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH v2 21/22] auto-installer: fetch: add http plugin to fetch answer
Date: Wed, 21 Feb 2024 12:08:04 +0100 [thread overview]
Message-ID: <20240221110805.931925-22-a.lauterer@proxmox.com> (raw)
In-Reply-To: <20240221110805.931925-1-a.lauterer@proxmox.com>
This plugin will send a HTTP POST request with identifying sysinfo to
fetch an answer file. The provided sysinfo can be used to identify the
system and generate a matching answer file on demand.
The URL to send the request to, can be defined in two ways. Via a custom
DHCP option or a TXT record on a predefined subdomain, relative to the
search domain received via DHCP.
Additionally it is possible to specify a SHA256 SSL fingerprint. This
can be useful if a self-signed certificate is used or the URL is using
an IP address instead of an FQDN. Even with a trusted cert, it can be
used to pin this specific certificate.
The certificate fingerprint can either be placed on the `proxmoxinst`
partition and needs to be called `cert_fingerprint.txt`, or it can be
provided in a second custom DHCP option or a TXT record.
Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
---
.../src/bin/proxmox-fetch-answer.rs | 11 +-
.../src/fetch_plugins/http.rs | 185 ++++++++++++++++++
.../src/fetch_plugins/mod.rs | 1 +
unconfigured.sh | 9 +
4 files changed, 203 insertions(+), 3 deletions(-)
create mode 100644 proxmox-auto-installer/src/fetch_plugins/http.rs
diff --git a/proxmox-auto-installer/src/bin/proxmox-fetch-answer.rs b/proxmox-auto-installer/src/bin/proxmox-fetch-answer.rs
index 9e89a3c..f5aeb92 100644
--- a/proxmox-auto-installer/src/bin/proxmox-fetch-answer.rs
+++ b/proxmox-auto-installer/src/bin/proxmox-fetch-answer.rs
@@ -1,6 +1,9 @@
use anyhow::{anyhow, Error, Result};
use log::{error, info, LevelFilter};
-use proxmox_auto_installer::{fetch_plugins::partition::FetchFromPartition, log::AutoInstLogger};
+use proxmox_auto_installer::{
+ fetch_plugins::{http::FetchFromHTTP, partition::FetchFromPartition},
+ log::AutoInstLogger,
+};
use std::io::Write;
use std::process::{Command, ExitCode, Stdio};
@@ -18,8 +21,10 @@ fn fetch_answer() -> Result<String> {
Ok(answer) => return Ok(answer),
Err(err) => info!("Fetching answer file from partition failed: {err}"),
}
- // TODO: add more options to get an answer file, e.g. download from url where url could be
- // fetched via txt records on predefined subdomain, kernel param, dhcp option, ...
+ match FetchFromHTTP::get_answer() {
+ Ok(answer) => return Ok(answer),
+ Err(err) => info!("Fetching answer file via HTTP failed: {err}"),
+ }
Err(Error::msg("Could not find any answer file!"))
}
diff --git a/proxmox-auto-installer/src/fetch_plugins/http.rs b/proxmox-auto-installer/src/fetch_plugins/http.rs
new file mode 100644
index 0000000..b9fdca7
--- /dev/null
+++ b/proxmox-auto-installer/src/fetch_plugins/http.rs
@@ -0,0 +1,185 @@
+use anyhow::{bail, Error, Result};
+use log::info;
+use std::{
+ fs::{self, read_to_string},
+ path::Path,
+ process::Command,
+};
+
+use crate::fetch_plugins::utils::{post, sysinfo};
+
+use super::utils;
+
+static CERT_FINGERPRINT_FILE: &str = "cert_fingerprint.txt";
+static ANSWER_SUBDOMAIN: &str = "proxmoxinst";
+static ANSWER_SUBDOMAIN_FP: &str = "proxmoxinst-fp";
+
+// It is possible to set custom DHPC options. Option numbers 224 to 254 [0].
+// To use them with dhclient, we need to configure it to request them and what they should be
+// called.
+//
+// e.g. /etc/dhcp/dhclient.conf:
+// ```
+// option proxmoxinst-url code 250 = text;
+// option proxmoxinst-fp code 251 = text;
+// also request proxmoxinst-url, proxmoxinst-fp;
+// ```
+//
+// The results will end up in the /var/lib/dhcp/dhclient.leases file from where we can fetch them
+//
+// [0] https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml
+static DHCP_URL_OPTION: &str = "proxmoxinst-url";
+static DHCP_FP_OPTION: &str = "proxmoxinst-fp";
+static DHCP_LEASE_FILE: &str = "/var/lib/dhcp/dhclient.leases";
+
+pub struct FetchFromHTTP;
+
+impl FetchFromHTTP {
+ /// Will try to fetch the answer.toml by sending a HTTP POST request. The URL can be configured
+ /// either via DHCP or DNS.
+ /// DHCP options are checked first. The SSL certificate need to be either trusted by the root
+ /// certs or a SHA256 fingerprint needs to be provided. The SHA256 SSL fingerprint can either
+ /// be placed in a `cert_fingerprint.txt` file in the `proxmoxinst` partition, as DHCP option,
+ /// or as DNS TXT record. If provided, the `cert_fingerprint.txt` file has preference.
+ pub fn get_answer() -> Result<String> {
+ info!("Checking for certificate fingerprint in file.");
+ let mut fingerprint: Option<String> = match Self::get_cert_fingerprint_from_file() {
+ Ok(fp) => Some(fp),
+ Err(err) => {
+ info!("{err}");
+ None
+ }
+ };
+
+ let answer_url: String;
+
+ (answer_url, fingerprint) = match Self::fetch_dhcp(fingerprint.clone()) {
+ Ok((url, fp)) => (url, fp),
+ Err(err) => {
+ info!("{err}");
+ Self::fetch_dns(fingerprint.clone())?
+ }
+ };
+
+ info!("Gathering system information.");
+ let payload = sysinfo::get_sysinfo(false)?;
+ info!("Sending POST request to '{answer_url}'.");
+ let answer = post::call(answer_url, fingerprint.as_deref(), payload)?;
+ Ok(answer)
+ }
+
+ /// Reads certificate fingerprint from file
+ pub fn get_cert_fingerprint_from_file() -> Result<String> {
+ let mount_path = utils::mount_proxmoxinst_part()?;
+ let cert_path = Path::new(mount_path.as_str()).join(CERT_FINGERPRINT_FILE);
+ match cert_path.try_exists() {
+ Ok(true) => {
+ info!("Found certifacte fingerprint file.");
+ Ok(fs::read_to_string(cert_path)?.trim().into())
+ }
+ _ => Err(Error::msg(format!(
+ "could not find cert fingerprint file expected at: {}",
+ cert_path.display()
+ ))),
+ }
+ }
+
+ /// Fetches search domain from resolv.conf file
+ fn get_search_domain() -> Result<String> {
+ info!("Retrieving default search domain.");
+ for line in read_to_string("/etc/resolv.conf")?.lines() {
+ if let Some((key, value)) = line.split_once(' ') {
+ if key == "search" {
+ return Ok(value.trim().into());
+ }
+ }
+ }
+ Err(Error::msg("Could not find search domain in resolv.conf."))
+ }
+
+ /// Runs a TXT DNS query on the domain provided
+ fn query_txt_record(query: String) -> Result<String> {
+ info!("Querying TXT record for '{query}'");
+ let url: String;
+ match Command::new("dig")
+ .args(["txt", "+short"])
+ .arg(&query)
+ .output()
+ {
+ Ok(output) => {
+ if output.status.success() {
+ url = String::from_utf8(output.stdout)?
+ .replace('"', "")
+ .trim()
+ .into();
+ if url.is_empty() {
+ bail!("Got empty response.");
+ }
+ } else {
+ bail!(
+ "Error querying DNS record '{query}' : {}",
+ String::from_utf8(output.stderr)?
+ );
+ }
+ }
+ Err(err) => bail!("Error querying DNS record '{query}': {err}"),
+ }
+ info!("Found: '{url}'");
+ Ok(url)
+ }
+
+ /// Tries to fetch answer URL and SSL fingerprint info from DNS
+ fn fetch_dns(mut fingerprint: Option<String>) -> Result<(String, Option<String>)> {
+ let search_domain = Self::get_search_domain()?;
+
+ let answer_url = match Self::query_txt_record(format!("{ANSWER_SUBDOMAIN}.{search_domain}"))
+ {
+ Ok(url) => url,
+ Err(err) => bail!("{err}"),
+ };
+
+ if fingerprint.is_none() {
+ fingerprint =
+ match Self::query_txt_record(format!("{ANSWER_SUBDOMAIN_FP}.{search_domain}")) {
+ Ok(fp) => Some(fp),
+ Err(err) => {
+ info!("{err}");
+ None
+ }
+ };
+ }
+ Ok((answer_url, fingerprint))
+ }
+
+ /// Tries to fetch answer URL and SSL fingerprint info from DHCP options
+ fn fetch_dhcp(mut fingerprint: Option<String>) -> Result<(String, Option<String>)> {
+ let leases = fs::read_to_string(DHCP_LEASE_FILE)?;
+
+ let mut answer_url: Option<String> = None;
+
+ let url_match = format!("option {DHCP_URL_OPTION}");
+ let fp_match = format!("option {DHCP_FP_OPTION}");
+
+ for line in leases.lines() {
+ if answer_url.is_none() && line.trim().starts_with(url_match.as_str()) {
+ answer_url = Self::strip_dhcp_option(line.split(' ').nth_back(0));
+ }
+ if fingerprint.is_none() && line.trim().starts_with(fp_match.as_str()) {
+ fingerprint = Self::strip_dhcp_option(line.split(' ').nth_back(0));
+ }
+ }
+
+ let answer_url = match answer_url {
+ None => bail!("No DHCP option found for fetch URL."),
+ Some(url) => url,
+ };
+
+ Ok((answer_url, fingerprint))
+ }
+
+ /// Clean DHCP option string
+ fn strip_dhcp_option(value: Option<&str>) -> Option<String> {
+ // value is expected to be in format: "value";
+ value.map(|value| String::from(&value[1..value.len() - 2]))
+ }
+}
diff --git a/proxmox-auto-installer/src/fetch_plugins/mod.rs b/proxmox-auto-installer/src/fetch_plugins/mod.rs
index 6f1e8a2..354fa7e 100644
--- a/proxmox-auto-installer/src/fetch_plugins/mod.rs
+++ b/proxmox-auto-installer/src/fetch_plugins/mod.rs
@@ -1,2 +1,3 @@
+pub mod http;
pub mod partition;
pub mod utils;
diff --git a/unconfigured.sh b/unconfigured.sh
index f668be4..fd0fb53 100755
--- a/unconfigured.sh
+++ b/unconfigured.sh
@@ -205,6 +205,15 @@ if [ $proxdebug -ne 0 ]; then
debugsh || true
fi
+# add custom DHCP options for auto installer
+if [ $proxauto -ne 0 ]; then
+ cat >> /etc/dhcp/dhclient.conf <<EOF
+option proxmoxinst-url code 250 = text;
+option proxmoxinst-fp code 251 = text;
+also request proxmoxinst-url, proxmoxinst-fp;
+EOF
+fi
+
# try to get ip config with dhcp
echo -n "Attempting to get DHCP leases... "
dhclient -v
--
2.39.2
next prev parent reply other threads:[~2024-02-21 11:08 UTC|newest]
Thread overview: 29+ messages / expand[flat|nested] mbox.gz Atom feed top
2024-02-21 11:07 [pve-devel] [PATCH v2 00/22] add automated/unattended installation Aaron Lauterer
2024-02-21 11:07 ` [pve-devel] [PATCH v2 01/22] tui: common: move InstallConfig struct to common crate Aaron Lauterer
2024-02-21 11:07 ` [pve-devel] [PATCH v2 02/22] common: make InstallZfsOption members public Aaron Lauterer
2024-02-21 11:07 ` [pve-devel] [PATCH v2 03/22] common: tui: use BTreeMap for predictable ordering Aaron Lauterer
2024-02-21 11:07 ` [pve-devel] [PATCH v2 04/22] low-level: add dump-udev command Aaron Lauterer
2024-02-21 11:07 ` [pve-devel] [PATCH v2 05/22] add auto-installer crate Aaron Lauterer
2024-02-21 11:07 ` [pve-devel] [PATCH v2 06/22] auto-installer: add dependencies Aaron Lauterer
2024-02-21 11:07 ` [pve-devel] [PATCH v2 07/22] auto-installer: add answer file definition Aaron Lauterer
2024-02-21 11:07 ` [pve-devel] [PATCH v2 08/22] auto-installer: add struct to hold udev info Aaron Lauterer
2024-02-21 11:07 ` [pve-devel] [PATCH v2 09/22] auto-installer: add utils Aaron Lauterer
2024-02-21 11:07 ` [pve-devel] [PATCH v2 10/22] auto-installer: add simple logging Aaron Lauterer
2024-02-21 11:07 ` [pve-devel] [PATCH v2 11/22] auto-installer: add tests for answer file parsing Aaron Lauterer
2024-02-21 11:07 ` [pve-devel] [PATCH v2 12/22] auto-installer: add auto-installer binary Aaron Lauterer
2024-02-21 11:07 ` [pve-devel] [PATCH v2 13/22] auto-installer: add fetch answer binary Aaron Lauterer
2024-02-21 11:07 ` [pve-devel] [PATCH v2 14/22] unconfigured: add proxauto as option to start auto installer Aaron Lauterer
2024-02-21 11:07 ` [pve-devel] [PATCH v2 15/22] auto-installer: use glob crate for pattern matching Aaron Lauterer
2024-02-21 11:07 ` [pve-devel] [PATCH v2 16/22] auto-installer: utils: make get_udev_index functions public Aaron Lauterer
2024-02-21 11:08 ` [pve-devel] [PATCH v2 17/22] auto-installer: add proxmox-autoinst-helper tool Aaron Lauterer
2024-02-21 11:08 ` [pve-devel] [PATCH v2 18/22] auto-installer: fetch: add gathering of system identifiers and restructure code Aaron Lauterer
2024-02-21 14:09 ` Christoph Heiss
2024-02-21 16:07 ` Aaron Lauterer
2024-02-21 11:08 ` [pve-devel] [PATCH v2 19/22] auto-installer: helper: add subcommand to view indentifiers Aaron Lauterer
2024-02-21 11:08 ` [pve-devel] [PATCH v2 20/22] auto-installer: fetch: add http post utility module Aaron Lauterer
2024-02-21 12:21 ` Christoph Heiss
2024-02-21 11:08 ` Aaron Lauterer [this message]
2024-02-21 11:08 ` [pve-devel] [PATCH v2 22/22] control: update build depends for auto installer Aaron Lauterer
2024-02-21 13:39 ` [pve-devel] [PATCH v2 00/22] add automated/unattended installation Christoph Heiss
2024-02-23 10:19 ` Friedrich Weber
2024-02-23 11:37 ` Aaron Lauterer
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20240221110805.931925-22-a.lauterer@proxmox.com \
--to=a.lauterer@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.