Files
wireguard-admin/src/app/commit.rs
T

142 lines
5.0 KiB
Rust

//! Saved-state Commit scenario.
//!
//! The ordering is deliberately explicit: prepare and validate the complete
//! candidate, archive the installed generation, install it through the
//! journaled store, then publish only already-prepared in-memory values.
use std::net::IpAddr;
use crate::backup::{BackupError, BackupReason, HistoricalApplyStatus, SavedBytes};
use crate::domain::ClientStates;
use crate::persistence::{BackupComplete, PersistedState, PersistenceError, SavedStore};
use crate::vault::VaultBundle;
use crate::wireguard::ConfigDocument;
use super::{AppError, Application, historical_apply_from_saved};
struct PreparedCommit {
proposed: SavedBytes,
document: ConfigDocument,
states: ClientStates,
}
impl Application {
/// Prepare a candidate without changing the live session or installed
/// files. All parsing, serialization and store validation finish here.
fn prepare_commit(
&self,
installed: &SavedBytes,
) -> Result<(SavedBytes, ConfigDocument, ClientStates), AppError> {
if let Some(existing_bytes) = &installed.vault {
let existing_text =
std::str::from_utf8(existing_bytes).map_err(|_| AppError::InvalidSavedState)?;
let existing = VaultBundle::from_toml(existing_text)?;
if self
.draft
.vault
.as_ref()
.is_none_or(|vault| existing.recipient() != vault.recipient())
{
return Err(AppError::VaultIdentityChange);
}
}
let mut states = self.draft.states.clone();
states.commit_draft();
let reservations = states
.reserved_addresses()
.iter()
.map(IpAddr::to_string)
.collect::<Vec<_>>();
let internal_state = Some(PersistedState::new(reservations, None)?.to_toml()?);
let vault = self
.draft
.vault
.as_ref()
.map(VaultBundle::to_toml)
.transpose()?
.map(String::into_bytes);
let proposed = SavedBytes {
settings: installed.settings.clone(),
server_config: self.draft.document.render().to_vec(),
internal_state,
vault,
};
self.store.validate_saved(&proposed)?;
// Reuse this parsed document after install; publication must not have a
// fallible parse step after the filesystem has already advanced.
let document = ConfigDocument::parse(&proposed.server_config)?;
Ok((proposed, document, states))
}
}
pub(super) fn commit(
application: &mut Application,
create_backup: impl FnOnce(
&std::path::Path,
BackupReason,
HistoricalApplyStatus,
&SavedBytes,
) -> Result<std::path::PathBuf, BackupError>,
install: impl FnOnce(&SavedStore, &SavedBytes, BackupComplete) -> Result<(), PersistenceError>,
) -> Result<std::path::PathBuf, AppError> {
let installed = application.store.read_saved()?;
let historical_apply = historical_apply_from_saved(&installed)?;
let (proposed, document, states) = application.prepare_commit(&installed)?;
let prepared = PreparedCommit {
proposed,
document,
states,
};
// Passing BackupComplete is the only path to installation, and this point
// is reached only after the full archive has been published successfully.
let archive = create_backup(
application.store.archives_directory(),
BackupReason::PreCommit,
historical_apply,
&installed,
)?;
if let Err(error) = install(&application.store, &prepared.proposed, BackupComplete(())) {
retain_first_issue_rights_after_failed_install(application);
return Err(error.into());
}
application.saved = prepared.proposed;
application.draft.states = prepared.states;
application.draft.document = prepared.document;
application
.first_delivery
.invalidate_active_authorizations();
application.first_delivery.promote_matching(
&application.draft.document,
application.draft.vault.as_ref(),
);
application.historical_apply = HistoricalApplyStatus::Unknown;
application.verified_saved_projection = None;
application.active_restart_attempt = None;
Ok(archive)
}
fn retain_first_issue_rights_after_failed_install(application: &mut Application) {
match application.store.read_saved() {
Ok(current) => {
let document = ConfigDocument::parse(&current.server_config).ok();
let vault = current
.vault
.as_deref()
.and_then(|bytes| std::str::from_utf8(bytes).ok())
.and_then(|text| VaultBundle::from_toml(text).ok());
if let Some(document) = document {
application
.first_delivery
.retain_matching(&document, vault.as_ref());
} else {
application.first_delivery.clear();
}
}
Err(_) => application.first_delivery.clear(),
}
}