Skip to main content

Mist/
Server.rs

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