#[cfg(feature = "test-util")]
pub(crate) mod fake;
#[cfg(target_os = "linux")]
pub(crate) mod linux;
#[cfg(target_os = "macos")]
pub(crate) mod macos;
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub(crate) mod text_config;
#[cfg(target_os = "windows")]
pub(crate) mod windows;
use serde::{Deserialize, Serialize};
use crate::capability::{BackendKind, Capabilities, MutationGuard, OwnershipIdentity};
use crate::config::{DnsConfig, DnsScope};
use crate::error::{Error, Result};
use crate::interface::InterfaceInfo;
use crate::normalize::NormalizedConfig;
use crate::ownership::ResourceId;
use crate::watch::{WatchCallback, WatchHandle};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub(crate) struct PlatformSnapshot {
pub(crate) backend: BackendKind,
pub(crate) resource: ResourceId,
pub(crate) data: serde_json::Value,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub(crate) struct ResourceIdentity {
pub(crate) backend: BackendKind,
pub(crate) resource: ResourceId,
pub(crate) data: serde_json::Value,
}
impl ResourceIdentity {
pub(crate) fn new(backend: BackendKind, resource: ResourceId, data: serde_json::Value) -> Self {
Self {
backend,
resource,
data,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
pub(crate) enum ResourceStatus {
Same,
Gone,
Replaced,
Ambiguous,
}
#[derive(Debug, Clone)]
pub(crate) struct BoundObservation {
pub(crate) identity: ResourceIdentity,
pub(crate) snapshot: PlatformSnapshot,
}
impl PlatformSnapshot {
#[allow(dead_code)]
pub(crate) fn new(backend: BackendKind, resource: ResourceId, data: serde_json::Value) -> Self {
Self {
backend,
resource,
data,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct OwnershipProof {
snapshot: PlatformSnapshot,
}
impl OwnershipProof {
pub(crate) fn issued(snapshot: PlatformSnapshot) -> Self {
Self { snapshot }
}
pub(crate) fn as_snapshot(&self) -> &PlatformSnapshot {
&self.snapshot
}
pub(crate) fn into_snapshot(self) -> PlatformSnapshot {
self.snapshot
}
}
#[derive(Debug, Clone)]
pub(crate) struct VerifiedMutation {
pub(crate) proof: Option<OwnershipProof>,
pub(crate) observed: PlatformSnapshot,
}
impl VerifiedMutation {
pub(crate) fn persist(&self) -> PlatformSnapshot {
self.proof
.as_ref()
.map(|proof| proof.as_snapshot().clone())
.unwrap_or_else(|| self.observed.clone())
}
}
#[derive(Debug, Clone)]
pub(crate) struct ApplyReceipt {
#[allow(dead_code)]
pub(crate) resource: ResourceId,
}
#[derive(Debug)]
pub(crate) enum MutationAttempt {
Performed { produced: Option<PlatformSnapshot> },
Rejected { error: Error },
Indeterminate {
error: Error,
produced: Option<PlatformSnapshot>,
},
}
impl MutationAttempt {
pub(crate) fn from_apply_result(result: Result<ApplyReceipt>) -> Self {
match result {
Ok(_) => Self::Performed { produced: None },
Err(error) if error.is_external_modification() => Self::Rejected { error },
Err(error) => Self::Indeterminate {
error,
produced: None,
},
}
}
}
pub(crate) trait Backend: Send + Sync {
fn kind(&self) -> BackendKind;
fn capabilities(&self) -> Capabilities;
fn resolve_resources(
&self,
scope: &DnsScope,
plan: &NormalizedConfig,
) -> Result<Vec<ResourceId>>;
fn list_interfaces(&self) -> Result<Vec<InterfaceInfo>>;
fn identify(&self, resource: &ResourceId) -> Result<ResourceIdentity> {
Ok(ResourceIdentity::new(
self.kind(),
resource.clone(),
serde_json::Value::Null,
))
}
fn resource_status(&self, identity: &ResourceIdentity) -> Result<ResourceStatus> {
if identity.backend != self.kind() || identity.resource.as_str().is_empty() {
return Err(Error::JournalCorrupt(
"resource identity backend/resource mismatch".to_string(),
));
}
Ok(ResourceStatus::Same)
}
fn observe(&self, resource: &ResourceId) -> Result<BoundObservation> {
let identity = self.identify(resource)?;
let snapshot = self.capture(resource)?;
if self.resource_status(&identity)? != ResourceStatus::Same {
return Err(Error::ResourceIdentity {
backend: self.kind(),
resource: resource.clone(),
message: "resource incarnation changed while it was being observed".to_string(),
});
}
Ok(BoundObservation { identity, snapshot })
}
fn apply_bound(
&self,
identity: &ResourceIdentity,
expected: &PlatformSnapshot,
plan: &NormalizedConfig,
) -> MutationAttempt {
match self.resource_status(identity) {
Ok(ResourceStatus::Same) => match self.mutation_guard() {
MutationGuard::CompareAndMutate => {
self.apply_guarded(&identity.resource, expected, plan)
}
MutationGuard::Unconditional => match self.readback(&identity.resource) {
Ok(current) if self.equivalent(expected, ¤t) => {
match self.resource_status(identity) {
Ok(ResourceStatus::Same) => MutationAttempt::from_apply_result(
self.apply(&identity.resource, plan),
),
Ok(status) => MutationAttempt::Rejected {
error: Error::ResourceIdentity {
backend: self.kind(),
resource: identity.resource.clone(),
message: format!(
"resource incarnation became {status:?} before mutation"
),
},
},
Err(error) => MutationAttempt::Rejected { error },
}
}
Ok(_) => MutationAttempt::Rejected {
error: Error::ExternalModification {
resource: identity.resource.clone(),
detail: "the current state changed since it was captured".to_string(),
},
},
Err(error) => MutationAttempt::Indeterminate {
error,
produced: None,
},
},
},
Ok(status) => MutationAttempt::Rejected {
error: Error::ResourceIdentity {
backend: self.kind(),
resource: identity.resource.clone(),
message: format!("resource incarnation is {status:?}; refusing mutation"),
},
},
Err(error) => MutationAttempt::Rejected { error },
}
}
fn restore_bound(
&self,
identity: &ResourceIdentity,
expected: &PlatformSnapshot,
target: &PlatformSnapshot,
) -> MutationAttempt {
match self.resource_status(identity) {
Ok(ResourceStatus::Same) => match self.mutation_guard() {
MutationGuard::CompareAndMutate => {
self.restore_guarded(&identity.resource, expected, target)
}
MutationGuard::Unconditional => match self.readback(&identity.resource) {
Ok(current) if self.owns_current(expected, ¤t) => {
match self.resource_status(identity) {
Ok(ResourceStatus::Same) => {
match self.restore(&identity.resource, target) {
Ok(()) => MutationAttempt::Performed { produced: None },
Err(error) => MutationAttempt::Indeterminate {
error,
produced: None,
},
}
}
Ok(status) => MutationAttempt::Rejected {
error: Error::ResourceIdentity {
backend: self.kind(),
resource: identity.resource.clone(),
message: format!(
"resource incarnation became {status:?} before restore"
),
},
},
Err(error) => MutationAttempt::Rejected { error },
}
}
Ok(_) => MutationAttempt::Rejected {
error: Error::ExternalModification {
resource: identity.resource.clone(),
detail: "the current state changed since ownership was verified"
.to_string(),
},
},
Err(error) => MutationAttempt::Indeterminate {
error,
produced: None,
},
},
},
Ok(status) => MutationAttempt::Rejected {
error: Error::ResourceIdentity {
backend: self.kind(),
resource: identity.resource.clone(),
message: format!("resource incarnation is {status:?}; refusing restore"),
},
},
Err(error) => MutationAttempt::Rejected { error },
}
}
fn capture(&self, resource: &ResourceId) -> Result<PlatformSnapshot>;
fn apply(&self, resource: &ResourceId, plan: &NormalizedConfig) -> Result<ApplyReceipt>;
fn readback(&self, resource: &ResourceId) -> Result<PlatformSnapshot>;
fn mutation_guard(&self) -> MutationGuard {
self.capabilities().mutation_guard
}
fn ownership_identity(&self) -> OwnershipIdentity {
self.capabilities().ownership_identity
}
fn owns_current(&self, claimed: &PlatformSnapshot, current: &PlatformSnapshot) -> bool {
match self.ownership_identity() {
OwnershipIdentity::Durable => self.proves_current(claimed, current),
OwnershipIdentity::BestEffort => self.equivalent(claimed, current),
}
}
fn apply_guarded(
&self,
_resource: &ResourceId,
_expected: &PlatformSnapshot,
_plan: &NormalizedConfig,
) -> MutationAttempt {
MutationAttempt::Rejected {
error: Error::unsupported(
self.kind(),
"this backend has no compare-and-mutate primitive",
),
}
}
fn restore(&self, resource: &ResourceId, snapshot: &PlatformSnapshot) -> Result<()>;
fn restore_guarded(
&self,
_resource: &ResourceId,
_expected: &PlatformSnapshot,
_target: &PlatformSnapshot,
) -> MutationAttempt {
MutationAttempt::Rejected {
error: Error::unsupported(
self.kind(),
"this backend has no compare-and-mutate primitive",
),
}
}
fn proves_current(&self, _proof: &PlatformSnapshot, _current: &PlatformSnapshot) -> bool {
false
}
fn equivalent(&self, a: &PlatformSnapshot, b: &PlatformSnapshot) -> bool;
fn matches_desired(&self, snapshot: &PlatformSnapshot, plan: &NormalizedConfig) -> bool;
fn validate_plan(&self, _scope: &DnsScope, _plan: &NormalizedConfig) -> Result<()> {
Ok(())
}
fn public_state(&self, snapshot: &PlatformSnapshot, scope: &DnsScope) -> Result<DnsConfig>;
fn start_watch(&self, callback: WatchCallback) -> Result<WatchHandle> {
let _ = callback;
Err(Error::unsupported(
self.kind(),
"this backend does not support change notifications",
))
}
fn flush_cache(&self) -> Result<()> {
Err(Error::unsupported(
self.kind(),
"this backend does not support cache flushing",
))
}
}
#[cfg_attr(not(feature = "test-util"), allow(dead_code))]
pub(crate) fn construct_backend(
kind: BackendKind,
owner: &str,
) -> Result<std::sync::Arc<dyn Backend>> {
use std::sync::Arc;
match kind {
#[cfg(target_os = "linux")]
BackendKind::SystemdResolved => {
linux::resolved::SystemdResolved::connect().map(|b| Arc::new(b) as Arc<dyn Backend>)
}
#[cfg(target_os = "linux")]
BackendKind::NetworkManager => linux::network_manager::NetworkManager::connect()
.map(|b| Arc::new(b) as Arc<dyn Backend>),
#[cfg(target_os = "linux")]
BackendKind::Resolvconf => {
let probe = linux::resolvconf::probe().ok_or_else(|| {
Error::BackendUnavailable("resolvconf/openresolv is not available".to_string())
})?;
Ok(Arc::new(linux::resolvconf::Resolvconf::new(probe, owner)))
}
#[cfg(target_os = "linux")]
BackendKind::ResolvConfFile => Ok(Arc::new(linux::direct::DirectResolvConf::new())),
#[cfg(target_os = "windows")]
BackendKind::WindowsIpHelper => Ok(Arc::new(windows::WindowsBackend::new(owner))),
#[cfg(target_os = "macos")]
BackendKind::MacosSystemConfiguration => Ok(Arc::new(macos::MacosBackend::new(owner))),
#[cfg(feature = "test-util")]
BackendKind::Fake => Ok(Arc::new(fake::FakeBackend::new())),
#[allow(unreachable_patterns)]
_ => Err(Error::BackendUnavailable(format!(
"{kind} is not available on this platform"
))),
}
}
pub(crate) fn select_default_backend(owner: &str) -> Result<std::sync::Arc<dyn Backend>> {
#[cfg(target_os = "linux")]
{
linux::detect::select(owner)
}
#[cfg(target_os = "macos")]
{
Ok(std::sync::Arc::new(macos::MacosBackend::new(owner)))
}
#[cfg(target_os = "windows")]
{
Ok(std::sync::Arc::new(windows::WindowsBackend::new(owner)))
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
let _ = owner;
Err(Error::BackendUnavailable(
"no platform backend is implemented for this target".to_string(),
))
}
}