Skip to main content

Crate osdns

Crate osdns 

Source
Expand description

osdns provides transactional, ownership-safe control over host operating-system DNS configuration on Linux, Windows, and macOS.

It is intended for VPN clients, mesh networks, local DNS proxies, tunnels, security agents, and other software that must modify the host resolver without taking ownership of unrelated system state.

osdns is not a DNS resolver, DNS server, or DNS protocol library. It configures the operating system’s resolver; it does not implement DNS itself.

§Ownership and incarnation safety

Refuse mutation when the backend’s available ownership or incarnation evidence no longer establishes a safe target.

DNS configuration is shared mutable state. DHCP clients, NetworkManager, systemd-resolved, other VPN software, administrators, and device-management tooling may change it at any time.

Every mutation belongs to an explicit owner and Lease, is journaled before it happens, and is restored only while the backend can still establish that the current state is the one this lease produced. Backends with OwnershipIdentity::Durable check a generation or version. Backends with OwnershipIdentity::BestEffort compare DNS values and then write; another actor can win the gap between those steps. Capabilities::mutation_guard is separate: it is whether the write itself can be refused when the expected state has already changed. Capabilities::resource_binding reports whether resource identity is native-guarded or only checked immediately before a native API that still permits a final selector-reuse race.

§Basic usage

use osdns::{DnsConfig, DnsManager, DnsScope, InterfaceSelector};

let manager = DnsManager::builder()
    .owner("io.example.agent")
    .build()?;

let config = DnsConfig::builder(DnsScope::Interface(InterfaceSelector::Default))
    .nameserver("127.0.0.1".parse().unwrap())
    .build()?;

manager.validate(&config)?;
let lease = manager.apply(&config)?;

// The configuration stays in effect while the lease is alive.
lease.restore()?;

§Leases

DnsManager::apply returns a Lease. The lease owns every OS resource covered by that operation — including resources where the desired state was already in effect, which still get journal records, live state, and reconciliation — and holds the corresponding inter-process locks for its lifetime. Inter-process locks live in a global system location independent of journal storage, so custom state directories never create private ownership universes.

Lease::restore is the canonical way to end a lease. Dropping a lease performs best-effort restoration, but correctness never depends on Drop: a crashed process is recovered through DnsManager::recover_stale.

A live lease can move to a new desired configuration with Lease::update without releasing ownership. An update cannot silently change the set of owned resources.

§Safe restoration

Restoration overwrites a resource only while the lease still owns it. With OwnershipIdentity::Durable, that means a backend-issued identity still names the applied state. With OwnershipIdentity::BestEffort, the backend compares DNS values and then writes; that sequence is not atomic. Capabilities::mutation_guard reports whether a write can be refused when the expected generation no longer matches. A state that merely matches the desired configuration proves nothing by itself.

§Crash recovery

Mutations are backed by a durable journal. The transaction order is:

capture -> write Prepared -> fsync -> apply -> read back -> verify
  -> write Applied -> fsync

A process crash may release an OS lock without removing its journal. DnsManager::recover_stale inspects records left behind by crashed or exited processes and recovers them where it is safe to do so. Recovery never guesses ownership: only an applied snapshot the backend still considers ours, or the original state, authorizes action. In particular, an unverified Prepared record whose current state merely matches the desired configuration proves nothing — the crash may predate the mutation while an external actor independently produced that state — so the resource is reported as RecoveryOutcome::ExternalConflict and left untouched. Corrupt current-format journals fail closed with Error::JournalCorrupt. Incompatible format versions fail with Error::UnsupportedJournalVersion. Pre-v1 journal state is not migrated; clear the old state directory before upgrading.

§Validation guarantee

DnsManager::validate success means the backend can faithfully represent every explicitly requested semantic. A backend never silently ignores a requested field: unsupported semantics fail with Error::Unsupported before any lock, journal write, or OS mutation. The pipeline is structural validation, then generic capability checks, then backend-specific semantic validation.

§Optional fields

None means preserve / leave unspecified, never implicitly false or empty. In particular, default_route = None preserves the current default-route value on every backend; only Some(true) / Some(false) may change it.

§Cooperative vs Enforce

ConflictPolicy::Cooperative (the default) does not intentionally overwrite state that its backend can identify as externally changed; conflicts are surfaced to the lease owner. Detection strength and any remaining native race are described by Capabilities.

ConflictPolicy::Enforce is for active VPN, mesh, and tunnel agents and actually guarantees active reconciliation without requiring a public DnsManager::watch subscription. The first active lease starts the internal native watch and reconciler; the last lease ending stops them. External changes to resources owned by a live lease are reconciled: the reconciler waits for stable authoritative state, rebases the lease onto the new external base, and reapplies the desired overlay transactionally. Restoring a rebased lease returns to the new external base, not the pre-lease state. Backends without watch support fail Enforce lease creation with Error::Unsupported instead of silently behaving cooperatively. DnsManager::watch remains a pure observability subscription.

§Update transactions

Lease::update moves every owned resource as one logical transaction: either all resources reach the new configuration or all are rolled back to their immediately previous applied state with journals restored. A valid configuration that resolves to a different resource set than the lease owns fails with Error::UpdateRequiresRebind; restore or abandon the lease and apply fresh.

§Split DNS

nameservers are the resolver endpoints owned by the configuration; routing_domains are the names that should route to those endpoints. When routing domains are non-empty and default_route != Some(true), unrelated DNS remains outside the overlay wherever the backend supports true split DNS. Backends follow ownership minimization: a split-only configuration owns only the scoped resources needed to express it (on macOS, only /etc/resolver/<domain> files, leaving the service DNS state untouched).

Routing domains are part of the platform-neutral configuration model (see DnsConfigBuilder::routing_domain). The mechanism depends on the active backend: systemd-resolved routing domains on Linux, NetworkManager DNS routing where supported (the root wildcard is the canonical ~.), NRPT rules on Windows, and scoped /etc/resolver/<domain> files on macOS. Configurations a backend cannot represent are rejected with Error::Unsupported before any mutation. Use DnsManager::capabilities to probe support at runtime.

§Platform and backend differences

Linux selects among systemd-resolved (per-link DNS and routing domains), NetworkManager (per-interface DNS), resolvconf/openresolv (owner-tagged global records), and direct /etc/resolv.conf manipulation, based on which component actually owns DNS state on the host.

Windows uses the modern IP Helper APIs for per-interface IPv4/IPv6 settings and the Name Resolution Policy Table (NRPT) for split DNS, with native IP Helper and registry notifications for watching. Windows has no global DNS scope. Requires Windows 10 build 19041 or later.

macOS uses SystemConfiguration for per-service DNS and scoped /etc/resolver/<domain> files for split DNS, with SCDynamicStore and FSEvents notifications for watching.

Capabilities is the authoritative runtime description of what the active backend guarantees. Never assume two backends behave identically.

§Privileges

Changing system DNS configuration generally requires elevated privileges. osdns never attempts privilege escalation. Insufficient permissions are reported as Error::RequiresPrivilege; the caller is responsible for running with appropriate OS privileges.

§Runtime model

osdns has no async runtime dependency and does not require Tokio or async-std. Configuration changes are synchronous control-plane operations using native blocking APIs. Native watcher threads are started for ConflictPolicy::Enforce leases (first active lease to last lease end) and for each DnsManager::watch subscription.

§Safety and security limitations

  • osdns never performs privilege escalation.
  • Filesystem and registry resources are ownership-controlled: files, rules, and records not demonstrably ours are never overwritten or deleted.
  • Corrupt or unknown journal state fails closed; no mutation is attempted.
  • Unsafe code is isolated to platform FFI modules and justified with SAFETY: comments.
  • DNS configuration alone does not enforce packet routing and is not DNS leak prevention. Applications requiring traffic isolation must separately control routing and firewall policy.

Re-exports§

pub use capability::BackendKind;
pub use capability::Capabilities;
pub use capability::MutationGuard;
pub use capability::OwnershipIdentity;
pub use capability::ResourceBinding;
pub use config::DnsConfig;
pub use config::DnsConfigBuilder;
pub use config::DnsScope;
pub use config::InterfaceSelector;
pub use error::ConflictReason;
pub use error::Error;
pub use error::Result;
pub use interface::InterfaceInfo;
pub use lease::Lease;
pub use lease::RestoreFailure;
pub use manager::ConflictPolicy;
pub use manager::DnsManager;
pub use manager::DnsManagerBuilder;
pub use manager::RecoveryOutcome;
pub use normalize::DnsSuffix;
pub use ownership::ResourceId;
pub use watch::DnsEvent;
pub use watch::WatchCallback;
pub use watch::WatchHandle;

Modules§

capability
Backend capability model: what each platform backend can guarantee.
config
Platform-neutral DNS configuration model with validated builders.
error
The typed error model.
interface
Network interface information.
lease
Leases: exclusive, transactional ownership over DNS state.
manager
The DnsManager entry point and builder.
normalize
DnsSuffix normalization and the normalized configuration form.
ownership
Resource identifiers and inter-process resource locking.
testing
Testing utilities for applications and tests built on osdns.
watch
Watch events and handles.