1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
//! `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
//!
//! ```no_run
//! use osdns::{DnsConfig, DnsManager, DnsScope, InterfaceSelector};
//!
//! # fn main() -> osdns::Result<()> {
//! 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()?;
//! # Ok(())
//! # }
//! ```
//!
//! # 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:
//!
//! ```text
//! 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`](crate::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.
/// Backend capability model: what each platform backend can guarantee.
/// Platform-neutral DNS configuration model with validated builders.
/// The typed error model.
/// Network interface information.
/// Leases: exclusive, transactional ownership over DNS state.
/// The [`DnsManager`] entry point and builder.
/// [`DnsSuffix`] normalization and the normalized configuration form.
/// Resource identifiers and inter-process resource locking.
/// Watch events and handles.
pub use ;
pub use ;
pub use ;
pub use InterfaceInfo;
pub use ;
pub use ;
pub use DnsSuffix;
pub use ResourceId;
pub use ;