Skip to main content

Mist/
Server.rs

1//! # DNS Server
2//!
3//! Builds and serves the private DNS catalog for CodeEditorLand.
4//! Binds exclusively to loopback (`127.0.0.1`) to prevent LAN exposure.
5
6use std::{
7	net::{IpAddr, Ipv4Addr, SocketAddr},
8	sync::Arc,
9};
10
11use anyhow::Result;
12// hickory-server 0.26 reorganisation:
13//   authority::*                       → zone_handler::*
14//   authority::Authority (trait)       → zone_handler::ZoneHandler
15//   authority::Catalog / ZoneType      → zone_handler::Catalog / ZoneType
16//   store::in_memory::InMemoryAuthority → store::in_memory::InMemoryZoneHandler
17//   server::ServerFuture               → Server (re-exported at crate root)
18//   InMemoryAuthority::empty(_,_,bool,_) → InMemoryZoneHandler::empty(_,_,AxfrPolicy,_)
19// The behaviour is unchanged; only names moved.
20use hickory_server::{
21	Server,
22	net::runtime::TokioRuntimeProvider,
23	store::in_memory::InMemoryZoneHandler,
24	zone_handler::{AxfrPolicy, Catalog, ZoneType},
25};
26use tokio::net::UdpSocket;
27
28/// Buffer capacity for outgoing DNS TCP responses per connection. 65 535 is
29/// the upper bound a single DNS message can reach over TCP (the 16-bit
30/// length prefix cap from RFC 1035 §4.2.2). Picking the cap avoids any
31/// truncation for zone-transfer or large TXT responses while staying well
32/// within memory for the dozen-or-so concurrent connections a local
33/// `land.playform.cloud` catalog ever sees.
34const DNS_TCP_RESPONSE_BUFFER_SIZE:usize = 65_535;
35
36/// Builds a DNS catalog for the CodeEditorLand private network.
37///
38/// Creates a catalog with an authoritative zone for `land.playform.cloud` that
39/// resolves all queries locally to loopback addresses.
40pub fn BuildCatalog(_DNSPort:u16) -> Result<Catalog> {
41	let mut Catalog = Catalog::new();
42
43	let EditorLandOrigin = hickory_proto::rr::Name::from_ascii("land.playform.cloud.").unwrap();
44
45	// `AxfrPolicy::Deny` replaces the old `false` bool that disabled AXFR.
46	// The trailing `None` is `Option<NxProofKind>` and remains dnssec-ring-gated.
47	// Turbofish pins the runtime provider so inference has a concrete type
48	// (the handler is generic over `P: RuntimeProvider`; there's no
49	// inference anchor without either an `.await`-driven callsite or an
50	// explicit parameter here).
51	let Authority = InMemoryZoneHandler::<TokioRuntimeProvider>::empty(
52		EditorLandOrigin.clone(),
53		ZoneType::Primary,
54		AxfrPolicy::Deny,
55		None,
56	);
57
58	let EditorLandLower = hickory_proto::rr::LowerName::from(&EditorLandOrigin);
59
60	let AuthorityArc = Arc::new(Authority);
61
62	Catalog.upsert(EditorLandLower, vec![AuthorityArc]);
63
64	Ok(Catalog)
65}
66
67/// Serves DNS queries on the specified loopback port (async).
68///
69/// Binds to `127.0.0.1:{Port}` for both UDP and TCP. Validates that the
70/// socket is bound to a loopback address before accepting connections.
71pub async fn Serve(Catalog:Catalog, Port:u16) -> Result<()> {
72	let Address:SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), Port);
73
74	let BindingIP = Address.ip();
75
76	match BindingIP {
77		IpAddr::V4(IP) => {
78			if !IP.is_loopback() {
79				return Err(anyhow::anyhow!(
80					"SECURITY: DNS server attempted to bind to non-loopback address: {}. Only 127.x.x.x addresses are \
81					 allowed.",
82					IP
83				));
84			}
85		},
86
87		IpAddr::V6(IP) if IP.is_loopback() => {},
88
89		_ => {
90			return Err(anyhow::anyhow!(
91				"SECURITY: DNS server attempted to bind to invalid address: {}. Only loopback addresses are allowed.",
92				BindingIP
93			));
94		},
95	}
96
97	tracing::info!("Binding DNS server to loopback address: {}", Address);
98
99	let UDPSocket = UdpSocket::bind(Address)
100		.await
101		.map_err(|E| anyhow::anyhow!("SECURITY: Failed to bind DNS server to {}: {}.", Address, E))?;
102
103	let BoundAddress = UDPSocket
104		.local_addr()
105		.map_err(|E| anyhow::anyhow!("SECURITY: Failed to retrieve bound socket address: {}", E))?;
106
107	if !BoundAddress.ip().is_loopback() {
108		return Err(anyhow::anyhow!(
109			"SECURITY: UDP socket bound to non-loopback address: {}.",
110			BoundAddress.ip()
111		));
112	}
113
114	// `Server` supersedes `ServerFuture`; constructor + register_* +
115	// block_until_done signatures are the same so the rest of this body is
116	// unchanged.
117	let mut Server = Server::new(Catalog);
118
119	Server.register_socket(UDPSocket);
120
121	let TCPListener = tokio::net::TcpListener::bind(Address)
122		.await
123		.map_err(|E| anyhow::anyhow!("SECURITY: Failed to bind TCP listener to {}: {}", Address, E))?;
124
125	let TCPBoundAddress = TCPListener
126		.local_addr()
127		.map_err(|E| anyhow::anyhow!("SECURITY: Failed to retrieve TCP listener bound address: {}", E))?;
128
129	if !TCPBoundAddress.ip().is_loopback() {
130		return Err(anyhow::anyhow!(
131			"SECURITY: TCP listener bound to non-loopback address: {}.",
132			TCPBoundAddress.ip()
133		));
134	}
135
136	Server.register_listener(TCPListener, std::time::Duration::from_secs(5), DNS_TCP_RESPONSE_BUFFER_SIZE);
137
138	tracing::info!("DNS server bound to loopback: UDP={}, TCP={}", BoundAddress, TCPBoundAddress);
139
140	match Server.block_until_done().await {
141		Ok(_) => {
142			tracing::info!("DNS server shutdown gracefully");
143
144			Ok(())
145		},
146
147		Err(E) => {
148			let ErrorMessage = format!("DNS server error: {:?}", E);
149
150			tracing::error!("{}", ErrorMessage);
151
152			Err(anyhow::anyhow!(ErrorMessage))
153		},
154	}
155}
156
157/// Serves DNS queries synchronously (blocking convenience wrapper).
158pub fn ServeSync(Catalog:Catalog, Port:u16) -> Result<()> {
159	let Runtime = tokio::runtime::Runtime::new()?;
160
161	Runtime.block_on(Serve(Catalog, Port))?;
162
163	Ok(())
164}
165
166#[cfg(test)]
167mod tests {
168
169	use hickory_proto::rr::Name;
170
171	use super::*;
172
173	#[test]
174	fn TestBuildCatalog() { let _Catalog = BuildCatalog(5353).expect("Failed to build catalog"); }
175
176	#[test]
177	fn TestSocketAddressIsLoopback() {
178		let Address:SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 5353);
179
180		assert!(Address.ip().is_loopback());
181	}
182}