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