1 use anyhow
::{bail, Error, Result}
;
4 fs
::{self, read_to_string}
,
9 use crate::fetch_plugins
::utils
::{post, sysinfo}
;
13 static CERT_FINGERPRINT_FILE
: &str = "cert_fingerprint.txt";
14 static ANSWER_SUBDOMAIN
: &str = "proxmoxinst";
15 static ANSWER_SUBDOMAIN_FP
: &str = "proxmoxinst-fp";
17 // It is possible to set custom DHPC options. Option numbers 224 to 254 [0].
18 // To use them with dhclient, we need to configure it to request them and what they should be
21 // e.g. /etc/dhcp/dhclient.conf:
23 // option proxmoxinst-url code 250 = text;
24 // option proxmoxinst-fp code 251 = text;
25 // also request proxmoxinst-url, proxmoxinst-fp;
28 // The results will end up in the /var/lib/dhcp/dhclient.leases file from where we can fetch them
30 // [0] https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml
31 static DHCP_URL_OPTION
: &str = "proxmoxinst-url";
32 static DHCP_FP_OPTION
: &str = "proxmoxinst-fp";
33 static DHCP_LEASE_FILE
: &str = "/var/lib/dhcp/dhclient.leases";
35 pub struct FetchFromHTTP
;
38 /// Will try to fetch the answer.toml by sending a HTTP POST request. The URL can be configured
39 /// either via DHCP or DNS.
40 /// DHCP options are checked first. The SSL certificate need to be either trusted by the root
41 /// certs or a SHA256 fingerprint needs to be provided. The SHA256 SSL fingerprint can either
42 /// be placed in a `cert_fingerprint.txt` file in the `proxmoxinst` partition, as DHCP option,
43 /// or as DNS TXT record. If provided, the `cert_fingerprint.txt` file has preference.
44 pub fn get_answer() -> Result
<String
> {
45 info
!("Checking for certificate fingerprint in file.");
46 let mut fingerprint
: Option
<String
> = match Self::get_cert_fingerprint_from_file() {
54 let answer_url
: String
;
56 (answer_url
, fingerprint
) = match Self::fetch_dhcp(fingerprint
.clone()) {
57 Ok((url
, fp
)) => (url
, fp
),
60 Self::fetch_dns(fingerprint
.clone())?
64 if fingerprint
.is_some() {
65 let fp
= fingerprint
.clone();
66 fs
::write("/tmp/cert_fingerprint", fp
.unwrap()).ok();
69 info
!("Gathering system information.");
70 let payload
= sysinfo
::get_sysinfo(false)?
;
71 info
!("Sending POST request to '{answer_url}'.");
72 let answer
= post
::call(answer_url
, fingerprint
.as_deref(), payload
)?
;
76 /// Reads certificate fingerprint from file
77 pub fn get_cert_fingerprint_from_file() -> Result
<String
> {
78 let mount_path
= utils
::mount_proxmoxinst_part()?
;
79 let cert_path
= Path
::new(mount_path
.as_str()).join(CERT_FINGERPRINT_FILE
);
80 match cert_path
.try_exists() {
82 info
!("Found certifacte fingerprint file.");
83 Ok(fs
::read_to_string(cert_path
)?
.trim().into())
85 _
=> Err(Error
::msg(format
!(
86 "could not find cert fingerprint file expected at: {}",
92 /// Fetches search domain from resolv.conf file
93 fn get_search_domain() -> Result
<String
> {
94 info
!("Retrieving default search domain.");
95 for line
in read_to_string("/etc/resolv.conf")?
.lines() {
96 if let Some((key
, value
)) = line
.split_once(' '
) {
98 return Ok(value
.trim().into());
102 Err(Error
::msg("Could not find search domain in resolv.conf."))
105 /// Runs a TXT DNS query on the domain provided
106 fn query_txt_record(query
: String
) -> Result
<String
> {
107 info
!("Querying TXT record for '{query}'");
109 match Command
::new("dig")
110 .args(["txt", "+short"])
115 if output
.status
.success() {
116 url
= String
::from_utf8(output
.stdout
)?
121 bail!("Got empty response
.");
125 "Error querying DNS record '{query}'
: {}
",
126 String::from_utf8(output.stderr)?
130 Err(err) => bail!("Error querying DNS record '{query}'
: {err}
"),
132 info!("Found
: '{url}'
");
136 /// Tries to fetch answer URL and SSL fingerprint info from DNS
137 fn fetch_dns(mut fingerprint: Option<String>) -> Result<(String, Option<String>)> {
138 let search_domain = Self::get_search_domain()?;
140 let answer_url = match Self::query_txt_record(format!("{ANSWER_SUBDOMAIN}
.{search_domain}
"))
143 Err(err) => bail!("{err}
"),
146 if fingerprint.is_none() {
148 match Self::query_txt_record(format!("{ANSWER_SUBDOMAIN_FP}
.{search_domain}
")) {
156 Ok((answer_url, fingerprint))
159 /// Tries to fetch answer URL and SSL fingerprint info from DHCP options
160 fn fetch_dhcp(mut fingerprint: Option<String>) -> Result<(String, Option<String>)> {
161 let leases = fs::read_to_string(DHCP_LEASE_FILE)?;
163 let mut answer_url: Option<String> = None;
165 let url_match = format!("option {DHCP_URL_OPTION}
");
166 let fp_match = format!("option {DHCP_FP_OPTION}
");
168 for line in leases.lines() {
169 if answer_url.is_none() && line.trim().starts_with(url_match.as_str()) {
170 answer_url = Self::strip_dhcp_option(line.split(' ').nth_back(0));
172 if fingerprint.is_none() && line.trim().starts_with(fp_match.as_str()) {
173 fingerprint = Self::strip_dhcp_option(line.split(' ').nth_back(0));
177 let answer_url = match answer_url {
178 None => bail!("No DHCP option found
for fetch URL
."),
182 Ok((answer_url, fingerprint))
185 /// Clean DHCP option string
186 fn strip_dhcp_option(value: Option<&str>) -> Option<String> {
187 // value is expected to be in format: "value
";
188 value.map(|value| String::from(&value[1..value.len() - 2]))