osdns/lib.rs
1#![deny(unsafe_op_in_unsafe_fn)]
2#![deny(missing_docs)]
3#![warn(clippy::all)]
4
5//! `osdns` provides transactional, ownership-safe control over host
6//! operating-system DNS configuration on Linux, Windows, and macOS.
7//!
8//! It is intended for VPN clients, mesh networks, local DNS proxies, tunnels,
9//! security agents, and other software that must modify the host resolver
10//! without taking ownership of unrelated system state.
11//!
12//! `osdns` is not a DNS resolver, DNS server, or DNS protocol library. It
13//! configures the operating system's resolver; it does not implement DNS
14//! itself.
15//!
16//! # Ownership and incarnation safety
17//!
18//! > Refuse mutation when the backend's available ownership or incarnation
19//! > evidence no longer establishes a safe target.
20//!
21//! DNS configuration is shared mutable state. DHCP clients, NetworkManager,
22//! systemd-resolved, other VPN software, administrators, and device-management
23//! tooling may change it at any time.
24//!
25//! Every mutation belongs to an explicit owner and [`Lease`], is journaled
26//! before it happens, and is restored only while the backend can still
27//! establish that the current state is the one this lease produced. Backends
28//! with [`OwnershipIdentity::Durable`] check a generation or version.
29//! Backends with [`OwnershipIdentity::BestEffort`] compare DNS values and
30//! then write; another actor can win the gap between those steps.
31//! [`Capabilities::mutation_guard`] is separate: it is whether the write
32//! itself can be refused when the expected state has already changed.
33//! [`Capabilities::resource_binding`] reports whether resource identity is
34//! native-guarded or only checked immediately before a native API that still
35//! permits a final selector-reuse race.
36//!
37//! # Basic usage
38//!
39//! ```no_run
40//! use osdns::{DnsConfig, DnsManager, DnsScope, InterfaceSelector};
41//!
42//! # fn main() -> osdns::Result<()> {
43//! let manager = DnsManager::builder()
44//! .owner("io.example.agent")
45//! .build()?;
46//!
47//! let config = DnsConfig::builder(DnsScope::Interface(InterfaceSelector::Default))
48//! .nameserver("127.0.0.1".parse().unwrap())
49//! .build()?;
50//!
51//! manager.validate(&config)?;
52//! let lease = manager.apply(&config)?;
53//!
54//! // The configuration stays in effect while the lease is alive.
55//! lease.restore()?;
56//! # Ok(())
57//! # }
58//! ```
59//!
60//! # Leases
61//!
62//! [`DnsManager::apply`] returns a [`Lease`]. The lease owns every OS resource
63//! covered by that operation — including resources where the desired state
64//! was already in effect, which still get journal records, live state, and
65//! reconciliation — and holds the corresponding inter-process locks for its
66//! lifetime. Inter-process locks live in a global system location
67//! independent of journal storage, so custom state directories never create
68//! private ownership universes.
69//!
70//! [`Lease::restore`] is the canonical way to end a lease. Dropping a lease
71//! performs best-effort restoration, but correctness never depends on `Drop`:
72//! a crashed process is recovered through
73//! [`DnsManager::recover_stale`].
74//!
75//! A live lease can move to a new desired configuration with
76//! [`Lease::update`] without releasing ownership. An update cannot silently
77//! change the set of owned resources.
78//!
79//! # Safe restoration
80//!
81//! Restoration overwrites a resource only while the lease still owns it.
82//! With [`OwnershipIdentity::Durable`], that means a backend-issued identity
83//! still names the applied state. With [`OwnershipIdentity::BestEffort`],
84//! the backend compares DNS values and then writes; that sequence is not
85//! atomic. [`Capabilities::mutation_guard`] reports whether a write can be
86//! refused when the expected generation no longer matches. A state that
87//! merely matches the desired configuration proves nothing by itself.
88//!
89//! # Crash recovery
90//!
91//! Mutations are backed by a durable journal. The transaction order is:
92//!
93//! ```text
94//! capture -> write Prepared -> fsync -> apply -> read back -> verify
95//! -> write Applied -> fsync
96//! ```
97//!
98//! A process crash may release an OS lock without removing its journal.
99//! [`DnsManager::recover_stale`] inspects records left behind by crashed or
100//! exited processes and recovers them where it is safe to do so. Recovery
101//! never guesses ownership: only an applied snapshot the backend still
102//! considers ours, or the original state, authorizes action. In particular, an unverified
103//! `Prepared` record whose current state merely matches the desired
104//! configuration proves nothing — the crash may predate the mutation while
105//! an external actor independently produced that state — so the resource is
106//! reported as [`RecoveryOutcome::ExternalConflict`] and left untouched.
107//! Corrupt current-format journals fail closed with [`Error::JournalCorrupt`].
108//! Incompatible format versions fail with
109//! [`Error::UnsupportedJournalVersion`]. Pre-v1 journal state is not migrated;
110//! clear the old state directory before upgrading.
111//!
112//! # Validation guarantee
113//!
114//! [`DnsManager::validate`] success means the backend can faithfully
115//! represent every explicitly requested semantic. A backend never silently
116//! ignores a requested field: unsupported semantics fail with
117//! [`Error::Unsupported`] before any lock, journal write, or OS mutation.
118//! The pipeline is structural validation, then generic capability checks,
119//! then backend-specific semantic validation.
120//!
121//! # Optional fields
122//!
123//! `None` means preserve / leave unspecified, never implicitly `false` or
124//! empty. In particular, `default_route = None` preserves the current
125//! default-route value on every backend; only `Some(true)` / `Some(false)`
126//! may change it.
127//!
128//! # Cooperative vs Enforce
129//!
130//! [`ConflictPolicy::Cooperative`] (the default) does not intentionally
131//! overwrite state that its backend can identify as externally changed;
132//! conflicts are surfaced to the lease owner. Detection strength and any
133//! remaining native race are described by [`Capabilities`].
134//!
135//! [`ConflictPolicy::Enforce`] is for active VPN, mesh, and tunnel agents
136//! and actually guarantees active reconciliation without requiring a public
137//! [`DnsManager::watch`] subscription. The first active lease starts the
138//! internal native watch and reconciler; the last lease ending stops them.
139//! External changes to resources owned by a live lease are reconciled: the
140//! reconciler waits for stable authoritative state, rebases the lease onto
141//! the new external base, and reapplies the desired overlay
142//! transactionally. Restoring a rebased lease returns to the new external
143//! base, not the pre-lease state. Backends without watch support fail
144//! Enforce lease creation with [`Error::Unsupported`] instead of silently
145//! behaving cooperatively. [`DnsManager::watch`] remains a pure
146//! observability subscription.
147//!
148//! # Update transactions
149//!
150//! [`Lease::update`] moves every owned resource as one logical transaction:
151//! either all resources reach the new configuration or all are rolled back
152//! to their immediately previous applied state with journals restored. A
153//! valid configuration that resolves to a different resource set than the
154//! lease owns fails with [`Error::UpdateRequiresRebind`]; restore or abandon
155//! the lease and apply fresh.
156//!
157//! # Split DNS
158//!
159//! `nameservers` are the resolver endpoints owned by the configuration;
160//! `routing_domains` are the names that should route to those endpoints.
161//! When routing domains are non-empty and `default_route != Some(true)`,
162//! unrelated DNS remains outside the overlay wherever the backend supports
163//! true split DNS. Backends follow ownership minimization: a split-only
164//! configuration owns only the scoped resources needed to express it (on
165//! macOS, only `/etc/resolver/<domain>` files, leaving the service DNS
166//! state untouched).
167//!
168//! Routing domains are part of the platform-neutral configuration model
169//! (see [`DnsConfigBuilder::routing_domain`](crate::DnsConfigBuilder::routing_domain)).
170//! The mechanism depends on the active backend: systemd-resolved routing
171//! domains on Linux, NetworkManager DNS routing where supported (the root
172//! wildcard is the canonical `~.`), NRPT rules on Windows, and scoped
173//! `/etc/resolver/<domain>` files on macOS.
174//! Configurations a backend cannot represent are rejected with
175//! [`Error::Unsupported`] before any mutation. Use
176//! [`DnsManager::capabilities`] to probe support at runtime.
177//!
178//! # Platform and backend differences
179//!
180//! Linux selects among systemd-resolved (per-link DNS and routing domains),
181//! NetworkManager (per-interface DNS), resolvconf/openresolv (owner-tagged
182//! global records), and direct `/etc/resolv.conf` manipulation, based on
183//! which component actually owns DNS state on the host.
184//!
185//! Windows uses the modern IP Helper APIs for per-interface IPv4/IPv6
186//! settings and the Name Resolution Policy Table (NRPT) for split DNS,
187//! with native IP Helper and registry notifications for watching. Windows
188//! has no global DNS scope. Requires Windows 10 build 19041 or later.
189//!
190//! macOS uses SystemConfiguration for per-service DNS and scoped
191//! `/etc/resolver/<domain>` files for split DNS, with SCDynamicStore and
192//! FSEvents notifications for watching.
193//!
194//! [`Capabilities`] is the authoritative runtime description of what the
195//! active backend guarantees. Never assume two backends behave identically.
196//!
197//! # Privileges
198//!
199//! Changing system DNS configuration generally requires elevated privileges.
200//! `osdns` never attempts privilege escalation. Insufficient permissions are
201//! reported as [`Error::RequiresPrivilege`]; the caller is responsible for
202//! running with appropriate OS privileges.
203//!
204//! # Runtime model
205//!
206//! `osdns` has no async runtime dependency and does not require Tokio or
207//! async-std. Configuration changes are synchronous control-plane operations
208//! using native blocking APIs. Native watcher threads are started for
209//! [`ConflictPolicy::Enforce`] leases (first active lease to last lease end)
210//! and for each [`DnsManager::watch`] subscription.
211//!
212//! # Safety and security limitations
213//!
214//! - `osdns` never performs privilege escalation.
215//! - Filesystem and registry resources are ownership-controlled: files, rules,
216//! and records not demonstrably ours are never overwritten or deleted.
217//! - Corrupt or unknown journal state fails closed; no mutation is attempted.
218//! - Unsafe code is isolated to platform FFI modules and justified with
219//! `SAFETY:` comments.
220//! - DNS configuration alone does not enforce packet routing and is not DNS
221//! leak prevention. Applications requiring traffic isolation must separately
222//! control routing and firewall policy.
223
224#[macro_use]
225mod macros;
226
227/// Backend capability model: what each platform backend can guarantee.
228pub mod capability;
229/// Platform-neutral DNS configuration model with validated builders.
230pub mod config;
231/// The typed error model.
232pub mod error;
233/// Network interface information.
234pub mod interface;
235/// Leases: exclusive, transactional ownership over DNS state.
236pub mod lease;
237/// The [`DnsManager`] entry point and builder.
238pub mod manager;
239/// [`DnsSuffix`] normalization and the normalized configuration form.
240pub mod normalize;
241/// Resource identifiers and inter-process resource locking.
242pub mod ownership;
243/// Watch events and handles.
244pub mod watch;
245
246#[cfg(feature = "test-util")]
247pub mod testing;
248
249mod fault;
250mod fsutil;
251mod journal;
252mod platform;
253mod reconciliation;
254
255pub use capability::{
256 BackendKind, Capabilities, MutationGuard, OwnershipIdentity, ResourceBinding,
257};
258pub use config::{DnsConfig, DnsConfigBuilder, DnsScope, InterfaceSelector};
259pub use error::{ConflictReason, Error, Result};
260pub use interface::InterfaceInfo;
261pub use lease::{Lease, RestoreFailure};
262pub use manager::{ConflictPolicy, DnsManager, DnsManagerBuilder, RecoveryOutcome};
263pub use normalize::DnsSuffix;
264pub use ownership::ResourceId;
265pub use watch::{DnsEvent, WatchCallback, WatchHandle};
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 #[test]
272 fn public_types_are_send_sync() {
273 fn assert_send_sync<T: Send + Sync>() {}
274 assert_send_sync::<DnsManager>();
275 assert_send_sync::<Lease>();
276 assert_send_sync::<Error>();
277 assert_send_sync::<DnsConfig>();
278 assert_send_sync::<DnsSuffix>();
279 assert_send_sync::<ResourceId>();
280 }
281}