From: Aaron Lauterer <a.lauterer@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH installer v4 24/30] auto-installer: fetch: add http plugin to fetch answer
Date: Thu, 4 Apr 2024 16:48:56 +0200 [thread overview]
Message-ID: <20240404144902.273800-25-a.lauterer@proxmox.com> (raw)
In-Reply-To: <20240404144902.273800-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 | 190 ++++++++++++++++++
.../src/fetch_plugins/mod.rs | 1 +
unconfigured.sh | 9 +
4 files changed, 208 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 a3681a2..6d42df2 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..4ac9afb
--- /dev/null
+++ b/proxmox-auto-installer/src/fetch_plugins/http.rs
@@ -0,0 +1,190 @@
+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())?
+ }
+ };
+
+ if fingerprint.is_some() {
+ let fp = fingerprint.clone();
+ fs::write("/tmp/cert_fingerprint", fp.unwrap()).ok();
+ }
+
+ 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 f02336a..dbdb027 100755
--- a/unconfigured.sh
+++ b/unconfigured.sh
@@ -212,6 +212,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-04-04 14:49 UTC|newest]
Thread overview: 34+ messages / expand[flat|nested] mbox.gz Atom feed top
2024-04-04 14:48 [pve-devel] [PATCH installer v4 00/30] add automated/unattended installation Aaron Lauterer
2024-04-04 14:48 ` [pve-devel] [PATCH installer v4 01/30] tui: common: move InstallConfig struct to common crate Aaron Lauterer
2024-04-04 14:48 ` [pve-devel] [PATCH installer v4 02/30] common: make InstallZfsOption members public Aaron Lauterer
2024-04-04 14:48 ` [pve-devel] [PATCH installer v4 03/30] common: tui: use BTreeMap for predictable ordering Aaron Lauterer
2024-04-04 14:48 ` [pve-devel] [PATCH installer v4 04/30] common: utils: add deserializer for CidrAddress Aaron Lauterer
2024-04-04 14:48 ` [pve-devel] [PATCH installer v4 05/30] common: options: add Deserialize trait Aaron Lauterer
2024-04-04 14:48 ` [pve-devel] [PATCH installer v4 06/30] low-level: add dump-udev command Aaron Lauterer
2024-04-04 14:48 ` [pve-devel] [PATCH installer v4 07/30] add auto-installer crate Aaron Lauterer
2024-04-04 14:48 ` [pve-devel] [PATCH installer v4 08/30] auto-installer: add dependencies Aaron Lauterer
2024-04-04 14:48 ` [pve-devel] [PATCH installer v4 09/30] auto-installer: add answer file definition Aaron Lauterer
2024-04-04 14:48 ` [pve-devel] [PATCH installer v4 10/30] auto-installer: add struct to hold udev info Aaron Lauterer
2024-04-04 14:48 ` [pve-devel] [PATCH installer v4 11/30] auto-installer: add utils Aaron Lauterer
2024-04-04 14:48 ` [pve-devel] [PATCH installer v4 12/30] auto-installer: add simple logging Aaron Lauterer
2024-04-04 14:48 ` [pve-devel] [PATCH installer v4 13/30] auto-installer: add tests for answer file parsing Aaron Lauterer
2024-04-04 14:48 ` [pve-devel] [PATCH installer v4 14/30] auto-installer: add auto-installer binary Aaron Lauterer
2024-04-04 14:48 ` [pve-devel] [PATCH installer v4 15/30] auto-installer: add fetch answer binary Aaron Lauterer
2024-04-04 14:48 ` [pve-devel] [PATCH installer v4 16/30] unconfigured: add proxauto as option to start auto installer Aaron Lauterer
2024-04-04 14:48 ` [pve-devel] [PATCH installer v4 17/30] auto-installer: use glob crate for pattern matching Aaron Lauterer
2024-04-04 14:48 ` [pve-devel] [PATCH installer v4 18/30] auto-installer: utils: make get_udev_index functions public Aaron Lauterer
2024-04-04 14:48 ` [pve-devel] [PATCH installer v4 19/30] auto-installer: add proxmox-autoinst-helper tool Aaron Lauterer
2024-04-04 14:48 ` [pve-devel] [PATCH installer v4 20/30] common: add Display trait to ProxmoxProduct Aaron Lauterer
2024-04-04 14:48 ` [pve-devel] [PATCH installer v4 21/30] auto-installer: fetch: add gathering of system identifiers and restructure code Aaron Lauterer
2024-04-04 14:48 ` [pve-devel] [PATCH installer v4 22/30] auto-installer: helper: add subcommand to view indentifiers Aaron Lauterer
2024-04-04 14:48 ` [pve-devel] [PATCH installer v4 23/30] auto-installer: fetch: add http post utility module Aaron Lauterer
2024-04-04 14:48 ` Aaron Lauterer [this message]
2024-04-04 14:48 ` [pve-devel] [PATCH installer v4 25/30] control: update build depends for auto installer Aaron Lauterer
2024-04-04 14:48 ` [pve-devel] [PATCH installer v4 26/30] auto installer: factor out fetch-answer and autoinst-helper Aaron Lauterer
2024-04-04 14:48 ` [pve-devel] [PATCH installer v4 27/30] low-level: write low level config to /tmp Aaron Lauterer
2024-04-04 14:49 ` [pve-devel] [PATCH installer v4 28/30] common: add deserializer for FsType Aaron Lauterer
2024-04-04 14:49 ` [pve-devel] [PATCH installer v4 29/30] common: skip target_hd when deserializing InstallConfig Aaron Lauterer
2024-04-04 14:49 ` [pve-devel] [PATCH installer v4 30/30] add proxmox-chroot utility Aaron Lauterer
2024-04-05 12:38 ` [pve-devel] [PATCH installer v4 00/30] add automated/unattended installation Christoph Heiss
2024-04-05 14:25 ` [pve-devel] [PATCH v4 installer 31/30 follow-up] auto-installer: answer: deny unknown fields Aaron Lauterer
2024-04-09 9:20 ` Christoph Heiss
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=20240404144902.273800-25-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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox