Skip to main content

mist/
lib.rs

1//! # Mist: Private DNS for Local-First Networking
2#![allow(clippy::tabs_in_doc_comments, clippy::unnecessary_lazy_evaluations)]
3//!
4//! Mist gives Land its own private DNS so editor components can find each
5//! other on `*.editor.land` without touching the public internet. All queries
6//! resolve to `127.0.0.1`. No external DNS leaks, no configuration needed.
7//!
8//! ## Features
9//!
10//! - **Private DNS Zone**: Authoritative zone for `*.editor.land` domains
11//! - **Local Resolution**: All editor.land queries resolve to `127.0.0.1`.
12//! - **Dynamic Port Allocation**: Automatically finds available ports using
13//!   portpicker
14//! - **Async/Sync Support**: Both async and blocking server implementations
15//!
16//! ## Example
17//!
18//! ```rust,no_run
19//! use Mist::start;
20//!
21//! #[tokio::main]
22//! async fn main() -> anyhow::Result<()> {
23//! 	// Start the DNS server (tries port 5353 first, then finds an available one)
24//! 	let port = start(5353)?;
25//!
26//! 	println!("DNS server running on port {}", port);
27//!
28//! 	// The server runs in the background
29//! 	// Use DNS_PORT to get the port number elsewhere
30//!
31//! 	Ok(())
32//! }
33//! ```
34
35use std::thread;
36
37use anyhow::Result;
38use once_cell::sync::OnceCell;
39
40// Public module exports (PascalCase per project convention)
41pub mod Server;
42pub mod Zone;
43pub mod Resolver;
44pub mod ForwardSecurity;
45
46/// Global DNS port number.
47///
48/// This static cell stores the port number that the DNS server is running on.
49/// It is set once when [`start`] is called and remains constant thereafter.
50///
51/// # Example
52///
53/// ```rust
54/// use Mist::dns_port;
55///
56/// // Returns the port number, or 0 if the server hasn't been started
57/// let port = dns_port();
58/// ```
59pub static DNS_PORT:OnceCell<u16> = OnceCell::new();
60
61/// Returns the DNS port number.
62///
63/// Returns the port that the DNS server is listening on, or `0` if the
64/// server has not been started yet.
65///
66/// # Returns
67///
68/// The port number (0-65535), or 0 if the server hasn't started.
69///
70/// # Example
71///
72/// ```rust
73/// use Mist::dns_port;
74///
75/// let port = dns_port();
76/// if port > 0 {
77/// 	println!("DNS server is running on port {}", port);
78/// } else {
79/// 	println!("DNS server has not been started");
80/// }
81/// ```
82pub fn dns_port() -> u16 { *DNS_PORT.get().unwrap_or(&0) }
83
84/// Starts the DNS server for the CodeEditorLand private network.
85///
86/// This function performs the following steps:
87/// 1. Uses portpicker to find an available port (tries `preferred_port` first)
88/// 2. Sets the `DNS_PORT` global variable
89/// 3. Builds the DNS catalog with the `editor.land` zone
90/// 4. Spawns the DNS server as a background task
91/// 5. Returns the port number
92///
93/// The DNS server runs in the background and can be stopped by dropping
94/// the application.
95///
96/// # Parameters
97///
98/// * `preferred_port` - The preferred port number to use. If this port is
99///   already in use, portpicker will find an alternative available port.
100///
101/// # Returns
102///
103/// Returns `Ok(port)` with the port number the server is listening on,
104/// or an error if the server failed to start.
105///
106/// # Example
107///
108/// ```rust,no_run
109/// use Mist::start;
110///
111/// #[tokio::main]
112/// async fn main() -> anyhow::Result<()> {
113///     // Start DNS server, preferring port 5353
114///     let port = start(5353)?;
115///     println!("DNS server started on port {}", port);
116///     tokio::signal::ctrl_c().await?;
117///     Ok(())
118/// }
119/// ```
120pub fn start(preferred_port: u16) -> Result<u16> {
121	// Step 1: Find an available port using portpicker
122	// Try the preferred port first, then pick a random available one
123	let port = portpicker::pick_unused_port()
124		.or_else(|| {
125			// If pick_unused_port returns None, try the preferred port explicitly
126			Some(preferred_port)
127		})
128		.ok_or_else(|| anyhow::anyhow!("Failed to find an available port"))?;
129
130	// Step 2: Set the DNS_PORT globally
131	DNS_PORT
132		.set(port)
133		.map_err(|_| anyhow::anyhow!("DNS port has already been set"))?;
134
135	// Step 3: Build the DNS catalog
136	let catalog = Server::BuildCatalog(port)?;
137
138	// Step 4: Spawn the DNS server as a background task
139	thread::spawn(move || {
140		if let Err(e) = Server::ServeSync(catalog, port) {
141			eprintln!("DNS server error: {:?}", e);
142		}
143	});
144
145	// Step 5: Return the port number
146	Ok(port)
147}
148
149#[cfg(test)]
150mod tests {
151	use super::*;
152
153	#[test]
154	fn test_dns_port_initial_state() {
155		// Initially, DNS_PORT should be 0
156		let port = dns_port();
157		assert_eq!(port, 0);
158	}
159
160	#[test]
161	fn test_dns_port_starts_server() {
162		// Test that we can start the DNS server
163		let preferred_port = 15353; // Use a non-standard port for testing
164
165		let result = start(preferred_port);
166
167		// The server should start successfully
168		assert!(result.is_ok(), "Failed to start DNS server");
169
170		let port = result.unwrap();
171
172		// The port should be within valid range
173		assert!(port >= 1024, "Port should be >= 1024");
174		assert!(port <= 65535, "Port should be <= 65535");
175
176		// DNS_PORT should now return the same port
177		let retrieved_port = dns_port();
178		assert_eq!(port, retrieved_port, "DNS_PORT should match returned port");
179	}
180
181	#[test]
182	fn test_start_fails_on_second_call() {
183		// Starting the server twice should fail
184		let port1 = start(15354);
185		assert!(port1.is_ok(), "First start should succeed");
186
187		let port2 = start(15355);
188		assert!(port2.is_err(), "Second start should fail");
189	}
190
191	#[test]
192	fn test_build_catalog_api() {
193		let catalog = Server::BuildCatalog(15356);
194		assert!(catalog.is_ok(), "Should be able to build catalog");
195	}
196
197	#[test]
198	fn test_build_zone_api() {
199		let zone = Zone::EditorLandZone();
200		assert!(zone.is_ok(), "Should be able to build zone");
201	}
202}