1 use anyhow
::{bail, Error, Result}
;
4 fs
::{self, read_to_string}
,
8 use proxmox_auto_installer
::{sysinfo::SysInfo, utils::AutoInstSettings}
;
10 static ANSWER_URL_SUBDOMAIN
: &str = "proxmox-auto-installer";
11 static ANSWER_CERT_FP_SUBDOMAIN
: &str = "proxmox-auto-installer-cert-fingerprint";
13 // It is possible to set custom DHPC options. Option numbers 224 to 254 [0].
14 // To use them with dhclient, we need to configure it to request them and what they should be
17 // e.g. /etc/dhcp/dhclient.conf:
19 // option proxmox-auto-installer-manifest-url code 250 = text;
20 // option proxmox-auto-installer-cert-fingerprint code 251 = text;
21 // also request proxmox-auto-installer-manifest-url, proxmox-auto-installer-cert-fingerprint;
24 // The results will end up in the /var/lib/dhcp/dhclient.leases file from where we can fetch them
26 // [0] https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml
27 static DHCP_URL_OPTION
: &str = "proxmox-auto-installer-manifest-url";
28 static DHCP_CERT_FP_OPTION
: &str = "proxmox-auto-installer-cert-fingerprint";
29 static DHCP_LEASE_FILE
: &str = "/var/lib/dhcp/dhclient.leases";
31 pub struct FetchFromHTTP
;
34 /// Will try to fetch the answer.toml by sending a HTTP POST request. The URL can be configured
35 /// either via DHCP or DNS or preconfigured in the ISO.
36 /// If the URL is not defined in the ISO, it will first check DHCP options. The SSL certificate
37 /// needs to be either trusted by the root certs or a SHA256 fingerprint needs to be provided.
38 /// The SHA256 SSL fingerprint can either be defined in the ISO, as DHCP option, or as DNS TXT
39 /// record. If provided, the fingerprint provided in the ISO has preference.
40 pub fn get_answer(settings
: &AutoInstSettings
) -> Result
<String
> {
41 let mut fingerprint
: Option
<String
> = match settings
.cert_fingerprint
.clone() {
43 info
!("SSL fingerprint provided through ISO.");
49 let answer_url
: String
;
50 if let Some(url
) = settings
.http_url
.clone() {
51 info
!("URL specified in ISO");
54 (answer_url
, fingerprint
) = match Self::fetch_dhcp(fingerprint
.clone()) {
55 Ok((url
, fp
)) => (url
, fp
),
58 Self::fetch_dns(fingerprint
.clone())?
63 if let Some(fingerprint
) = &fingerprint
{
64 let _
= fs
::write("/tmp/cert_fingerprint", fingerprint
);
67 info
!("Gathering system information.");
68 let payload
= SysInfo
::as_json()?
;
69 info
!("Sending POST request to '{answer_url}'.");
70 let answer
= http_post
::call(answer_url
, fingerprint
.as_deref(), payload
)?
;
74 /// Fetches search domain from resolv.conf file
75 fn get_search_domain() -> Result
<String
> {
76 info
!("Retrieving default search domain.");
77 for line
in read_to_string("/etc/resolv.conf")?
.lines() {
78 if let Some((key
, value
)) = line
.split_once(' '
) {
80 return Ok(value
.trim().into());
84 Err(Error
::msg("Could not find search domain in resolv.conf."))
87 /// Runs a TXT DNS query on the domain provided
88 fn query_txt_record(query
: String
) -> Result
<String
> {
89 info
!("Querying TXT record for '{query}'");
91 match Command
::new("dig")
92 .args(["txt", "+short"])
97 if output
.status
.success() {
98 url
= String
::from_utf8(output
.stdout
)?
103 bail!("Got empty response
.");
107 "Error querying DNS record '{query}'
: {}
",
108 String::from_utf8(output.stderr)?
112 Err(err) => bail!("Error querying DNS record '{query}'
: {err}
"),
114 info!("Found
: '{url}'
");
118 /// Tries to fetch answer URL and SSL fingerprint info from DNS
119 fn fetch_dns(mut fingerprint: Option<String>) -> Result<(String, Option<String>)> {
120 let search_domain = Self::get_search_domain()?;
123 match Self::query_txt_record(format!("{ANSWER_URL_SUBDOMAIN}
.{search_domain}
")) {
125 Err(err) => bail!("{err}
"),
128 if fingerprint.is_none() {
130 match Self::query_txt_record(format!("{ANSWER_CERT_FP_SUBDOMAIN}
.{search_domain}
"))
139 Ok((answer_url, fingerprint))
142 /// Tries to fetch answer URL and SSL fingerprint info from DHCP options
143 fn fetch_dhcp(mut fingerprint: Option<String>) -> Result<(String, Option<String>)> {
144 info!("Checking DHCP options
.");
145 let leases = fs::read_to_string(DHCP_LEASE_FILE)?;
147 let mut answer_url: Option<String> = None;
149 let url_match = format!("option {DHCP_URL_OPTION}
");
150 let fp_match = format!("option {DHCP_CERT_FP_OPTION}
");
152 for line in leases.lines() {
153 if answer_url.is_none() && line.trim().starts_with(url_match.as_str()) {
154 answer_url = Self::strip_dhcp_option(line.split(' ').nth_back(0));
156 if fingerprint.is_none() && line.trim().starts_with(fp_match.as_str()) {
157 fingerprint = Self::strip_dhcp_option(line.split(' ').nth_back(0));
161 let answer_url = match answer_url {
162 None => bail!("No DHCP option found
for fetch URL
."),
164 info!("Found URL
for answer
in DHCP option
: '{url}'
");
169 if let Some(fp) = fingerprint.clone() {
170 info!("Found SSL Fingerprint via DHCP
: '{fp}'
");
173 Ok((answer_url, fingerprint))
176 /// Clean DHCP option string
177 fn strip_dhcp_option(value: Option<&str>) -> Option<String> {
178 // value is expected to be in format: "value
";
179 value.map(|value| String::from(&value[1..value.len() - 2]))
185 use rustls::ClientConfig;
186 use sha2::{Digest, Sha256};
188 use ureq::{Agent, AgentBuilder};
190 /// Issues a POST request with the payload (JSON). Optionally a SHA256 fingerprint can be used to
191 /// check the cert against it, instead of the regular cert validation.
192 /// To gather the sha256 fingerprint you can use the following command:
194 /// openssl s_client -connect <host>:443 < /dev/null 2>/dev/null | openssl x509 -fingerprint -sha256 -noout -in /dev/stdin
198 /// * `url` - URL to call
199 /// * `fingerprint` - SHA256 cert fingerprint if certificate pinning should be used. Optional.
200 /// * `payload` - The payload to send to the server. Expected to be a JSON formatted string.
201 pub fn call(url: String, fingerprint: Option<&str>, payload: String) -> Result<String> {
204 if let Some(fingerprint) = fingerprint {
205 let tls_config = ClientConfig::builder()
206 .with_safe_defaults()
207 .with_custom_certificate_verifier(VerifyCertFingerprint::new(fingerprint)?)
208 .with_no_client_auth();
210 let agent: Agent = AgentBuilder::new().tls_config(Arc::new(tls_config)).build();
214 .set("Content
-type", "application
/json
; charset
=utf
-")
215 .send_string(&payload)?
218 let mut roots = rustls::RootCertStore::empty();
219 for cert in rustls_native_certs::load_native_certs()? {
220 roots.add(&rustls::Certificate(cert.0)).unwrap();
223 let tls_config = rustls::ClientConfig::builder()
224 .with_safe_defaults()
225 .with_root_certificates(roots)
226 .with_no_client_auth();
228 let agent = AgentBuilder::new()
229 .tls_connector(Arc::new(native_tls::TlsConnector::new()?))
230 .tls_config(Arc::new(tls_config))
234 .set("Content
-type", "application
/json
; charset
=utf
-")
235 .timeout(std::time::Duration::from_secs(60))
236 .send_string(&payload)?
242 struct VerifyCertFingerprint {
243 cert_fingerprint: Vec<u8>,
246 impl VerifyCertFingerprint {
247 fn new<S: AsRef<str>>(cert_fingerprint: S) -> Result<std::sync::Arc<Self>> {
248 let cert_fingerprint = cert_fingerprint.as_ref();
249 let sanitized = cert_fingerprint.replace(':', "");
250 let decoded = hex::decode(sanitized)?;
251 Ok(std::sync::Arc::new(Self {
252 cert_fingerprint: decoded,
257 impl rustls::client::ServerCertVerifier for VerifyCertFingerprint {
258 fn verify_server_cert(
260 end_entity: &rustls::Certificate,
261 _intermediates: &[rustls::Certificate],
262 _server_name: &rustls::ServerName,
263 _scts: &mut dyn Iterator<Item = &[u8]>,
264 _ocsp_response: &[u8],
265 _now: std::time::SystemTime,
266 ) -> Result<rustls::client::ServerCertVerified, rustls::Error> {
267 let mut hasher = Sha256::new();
268 hasher.update(end_entity);
269 let result = hasher.finalize();
271 if result.as_slice() == self.cert_fingerprint {
272 Ok(rustls::client::ServerCertVerified::assertion())
274 Err(rustls::Error::General("Fingerprint did not
match!".into()))