1use pharos_proto::{Message, Name, Opcode};
4use pharos_server::metrics::ProxyMetrics;
5use std::{
6 io,
7 net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
8 sync::{
9 Arc, Mutex,
10 atomic::{AtomicU32, Ordering},
11 },
12 time::Duration,
13};
14use tokio::{
15 io::{AsyncReadExt, AsyncWriteExt},
16 net::UdpSocket,
17 task::JoinHandle,
18 time::{sleep, timeout},
19};
20
21const DIAL_TIMEOUT: Duration = Duration::from_secs(2);
22const IO_TIMEOUT: Duration = Duration::from_secs(1);
23
24#[derive(Clone)]
26pub struct HealthConfig {
27 pub address: SocketAddr,
28 pub domain: Name,
29 pub recursive: bool,
30 pub force_tcp: bool,
31 pub source_address: Option<IpAddr>,
32 pub max_fails: u32,
33 pub interval: Duration,
34 pub metrics: Option<ProxyMetrics>,
35}
36
37impl HealthConfig {
38 #[must_use]
40 pub fn new(address: SocketAddr, max_fails: u32) -> Self {
41 Self {
42 address,
43 domain: ".".parse().expect("root is a valid DNS name"),
44 recursive: true,
45 force_tcp: false,
46 source_address: None,
47 max_fails,
48 interval: Duration::from_millis(500),
49 metrics: None,
50 }
51 }
52}
53
54#[derive(Default)]
55struct State {
56 running: bool,
57 shutdown: bool,
58 generation: u64,
59 task: Option<JoinHandle<()>>,
60}
61
62struct Shared {
63 config: HealthConfig,
64 state: Mutex<State>,
65 fails: AtomicU32,
66}
67
68pub struct Health {
70 shared: Arc<Shared>,
71}
72
73impl Health {
74 pub(super) fn config(&self) -> HealthConfig {
75 self.shared.config.clone()
76 }
77 #[must_use]
78 pub fn new(address: SocketAddr, max_fails: u32) -> Self {
79 Self::with_config(HealthConfig::new(address, max_fails))
80 }
81
82 #[must_use]
83 pub fn with_config(config: HealthConfig) -> Self {
84 Self {
85 shared: Arc::new(Shared {
86 config,
87 state: Mutex::new(State::default()),
88 fails: AtomicU32::new(0),
89 }),
90 }
91 }
92
93 pub fn start(&self) {
96 if self.shared.config.max_fails == 0 {
97 return;
98 }
99 let mut state = self.shared.state.lock().unwrap();
100 if state.shutdown {
101 return;
102 }
103 if state.running && state.task.as_ref().is_some_and(|task| !task.is_finished()) {
104 return;
105 }
106 state.running = true;
107 state.generation = state.generation.wrapping_add(1);
108 let generation = state.generation;
109 let shared = Arc::clone(&self.shared);
110 state.task = Some(tokio::spawn(async move {
111 loop {
112 let healthy = check(&shared.config).await.is_ok();
113 {
114 let mut state = shared.state.lock().unwrap();
115 if state.generation != generation {
116 return;
117 }
118 if healthy {
119 shared.fails.store(0, Ordering::Relaxed);
120 state.running = false;
121 return;
122 }
123 let _ =
124 shared
125 .fails
126 .try_update(Ordering::Relaxed, Ordering::Relaxed, |fails| {
127 Some(fails.saturating_add(1))
128 });
129 if let Some(metrics) = &shared.config.metrics {
130 metrics.healthcheck_failure();
131 }
132 }
133 sleep(shared.config.interval).await;
134 }
135 }));
136 }
137
138 #[must_use]
140 pub fn down(&self) -> bool {
141 self.shared.config.max_fails != 0 && self.fails() > self.shared.config.max_fails
142 }
143
144 #[must_use]
146 pub fn fails(&self) -> u32 {
147 self.shared.fails.load(Ordering::Relaxed)
148 }
149
150 pub fn stop(&self) {
152 let mut state = self.shared.state.lock().unwrap();
153 if let Some(task) = state.task.take() {
154 task.abort();
155 }
156 state.running = false;
157 state.generation = state.generation.wrapping_add(1);
158 }
159
160 pub(super) fn shutdown(&self) {
161 let mut state = self.shared.state.lock().unwrap();
162 state.shutdown = true;
163 if let Some(task) = state.task.take() {
164 task.abort();
165 }
166 state.running = false;
167 state.generation = state.generation.wrapping_add(1);
168 }
169}
170
171impl Drop for Health {
172 fn drop(&mut self) {
173 self.stop();
174 }
175}
176
177async fn check(config: &HealthConfig) -> io::Result<()> {
178 let mut id = [0; 2];
180 getrandom::getrandom(&mut id).map_err(|_| io::Error::other("random ID generation failed"))?;
181 let mut query = Vec::with_capacity(12 + config.domain.as_wire().len() + 4);
182 query.extend_from_slice(&id);
183 query.extend_from_slice(&[u8::from(config.recursive), 0, 0, 1, 0, 0, 0, 0, 0, 0]);
185 query.extend_from_slice(config.domain.as_wire());
186 query.extend_from_slice(&[0, 2, 0, 1]);
189
190 let response = if config.force_tcp {
191 let mut stream = timeout(
192 DIAL_TIMEOUT,
193 super::connect_tcp(config.address, config.source_address),
194 )
195 .await??;
196 timeout(IO_TIMEOUT, async {
197 stream
199 .write_all(&(query.len() as u16).to_be_bytes())
200 .await?;
201 stream.write_all(&query).await
202 })
203 .await??;
204 timeout(IO_TIMEOUT, async {
205 let len = stream.read_u16().await?;
207 let mut response = vec![0; usize::from(len)];
208 stream.read_exact(&mut response).await?;
209 Ok::<_, io::Error>(response)
210 })
211 .await??
212 } else {
213 let ip = if config.address.is_ipv4() {
214 IpAddr::V4(Ipv4Addr::UNSPECIFIED)
215 } else {
216 IpAddr::V6(Ipv6Addr::UNSPECIFIED)
217 };
218 let socket = timeout(DIAL_TIMEOUT, async {
219 let socket =
220 UdpSocket::bind(SocketAddr::new(config.source_address.unwrap_or(ip), 0)).await?;
221 super::enable_recv_err(&socket, config.address);
222 socket.connect(config.address).await?;
223 Ok::<_, io::Error>(socket)
224 })
225 .await??;
226 timeout(IO_TIMEOUT, socket.send(&query)).await??;
227 let mut response = vec![0; usize::from(u16::MAX)];
228 let len = timeout(IO_TIMEOUT, socket.recv(&mut response)).await??;
229 response.truncate(len);
230 response
231 };
232
233 if Message::parse(&response).is_ok()
235 || (response.len() >= 12
236 && (response[2] & 0x80 != 0 || (response[2] >> 3) & 15 == Opcode::QUERY.0))
237 {
238 Ok(())
239 } else {
240 Err(io::ErrorKind::InvalidData.into())
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use pharos_proto::{Class, Message, Type};
248 use tokio::{
249 net::{TcpListener, UdpSocket},
250 time::timeout,
251 };
252
253 #[tokio::test]
254 async fn default_probe_sends_recursive_root_ns_over_udp() {
255 let server = UdpSocket::bind("127.0.0.1:0").await.unwrap();
256 let health = Health::new(server.local_addr().unwrap(), 2);
257 health.start();
258 let mut wire = [0; 512];
259 let (len, peer) = timeout(Duration::from_secs(1), server.recv_from(&mut wire))
260 .await
261 .unwrap()
262 .unwrap();
263 let message = Message::parse(&wire[..len]).unwrap();
264 let question = message.questions().next().unwrap();
265 assert_eq!(question.name.to_string(), ".");
266 assert_eq!(question.qtype, Type::NS);
267 assert_eq!(question.qclass, Class::IN);
268 assert!(message.rd());
269 wire[2] |= 0x80;
270 server.send_to(&wire[..len], peer).await.unwrap();
271 }
272
273 #[tokio::test]
274 async fn failed_probe_updates_injected_metric_and_recovery_resets_fails() {
275 let server = UdpSocket::bind("127.0.0.1:0").await.unwrap();
276 let registry = pharos_server::metrics::Registry::new("test", "test", "test");
277 let mut config = HealthConfig::new(server.local_addr().unwrap(), 1);
278 config.metrics = Some(registry.proxy("forward", "test-upstream"));
279 config.interval = Duration::from_millis(25);
280 let health = Health::with_config(config);
281 health.start();
282 let mut wire = [0; 512];
283 let (_, peer) = timeout(Duration::from_secs(1), server.recv_from(&mut wire))
284 .await
285 .unwrap()
286 .unwrap();
287 server.send_to(&[0, 1], peer).await.unwrap();
288 timeout(Duration::from_secs(1), async {
289 while health.fails() < 1 {
290 tokio::task::yield_now().await;
291 }
292 })
293 .await
294 .unwrap();
295 assert!(!health.down());
296 let (len, peer) = timeout(Duration::from_secs(1), server.recv_from(&mut wire))
297 .await
298 .unwrap()
299 .unwrap();
300 server.send_to(&[0, 1], peer).await.unwrap();
301 timeout(Duration::from_secs(1), async {
302 while !health.down() {
303 tokio::task::yield_now().await;
304 }
305 })
306 .await
307 .unwrap();
308 let (len2, peer) = timeout(Duration::from_secs(1), server.recv_from(&mut wire))
309 .await
310 .unwrap()
311 .unwrap();
312 assert_eq!(len, len2);
313 wire[2] |= 0x80;
314 wire[3] = 2; server.send_to(&wire[..len2], peer).await.unwrap();
316 timeout(Duration::from_secs(1), async {
317 while health.fails() != 0 {
318 tokio::task::yield_now().await;
319 }
320 })
321 .await
322 .unwrap();
323 let mut text = String::new();
324 registry.encode(&mut text);
325 assert!(text.contains(
326 "pharos_proxy_healthcheck_failures_total{proxy_name=\"forward\",to=\"test-upstream\"} 2"
327 ));
328 assert!(!health.down());
329 }
330
331 #[tokio::test]
332 async fn configured_probe_sends_nonrecursive_name_over_tcp() {
333 let server = TcpListener::bind("127.0.0.1:0").await.unwrap();
334 let mut config = HealthConfig::new(server.local_addr().unwrap(), 2);
335 config.domain = "example.org.".parse().unwrap();
336 config.recursive = false;
337 config.force_tcp = true;
338 let health = Health::with_config(config);
339 health.start();
340 let (mut stream, _) = timeout(Duration::from_secs(1), server.accept())
341 .await
342 .unwrap()
343 .unwrap();
344 let len = stream.read_u16().await.unwrap();
345 let mut wire = vec![0; usize::from(len)];
346 stream.read_exact(&mut wire).await.unwrap();
347 let message = Message::parse(&wire).unwrap();
348 let question = message.questions().next().unwrap();
349 assert_eq!(question.name.to_string(), "example.org.");
350 assert_eq!(question.qtype, Type::NS);
351 assert_eq!(question.qclass, Class::IN);
352 assert!(!message.rd());
353 wire[2] |= 0x80;
354 wire[3] = 5;
355 stream.write_u16(len).await.unwrap();
356 stream.write_all(&wire).await.unwrap();
357 }
358
359 #[tokio::test]
360 async fn repeated_starts_share_one_probe_and_stop_closes_it() {
361 let server = TcpListener::bind("127.0.0.1:0").await.unwrap();
362 let mut config = HealthConfig::new(server.local_addr().unwrap(), 1);
363 config.force_tcp = true;
364 let health = Health::with_config(config);
365 health.start();
366 let (mut stream, _) = timeout(Duration::from_secs(1), server.accept())
367 .await
368 .unwrap()
369 .unwrap();
370 let len = stream.read_u16().await.unwrap();
371 let mut wire = vec![0; usize::from(len)];
372 stream.read_exact(&mut wire).await.unwrap();
373 for _ in 0..10 {
374 health.start();
375 }
376 assert!(
377 timeout(Duration::from_millis(50), server.accept())
378 .await
379 .is_err()
380 );
381 health.stop();
382 assert_eq!(
383 timeout(Duration::from_secs(1), stream.read(&mut wire))
384 .await
385 .unwrap()
386 .unwrap(),
387 0
388 );
389 health.start();
390 timeout(Duration::from_secs(1), server.accept())
391 .await
392 .unwrap()
393 .unwrap();
394 }
395
396 #[tokio::test]
397 async fn zero_max_fails_disables_checks() {
398 let server = UdpSocket::bind("127.0.0.1:0").await.unwrap();
399 let health = Health::new(server.local_addr().unwrap(), 0);
400 health.start();
401 let mut wire = [0; 512];
402 assert!(
403 timeout(Duration::from_millis(50), server.recv_from(&mut wire))
404 .await
405 .is_err()
406 );
407 assert_eq!(health.fails(), 0);
408 assert!(!health.down());
409 }
410
411 #[tokio::test]
412 async fn tcp_read_timeout_counts_as_a_failed_probe() {
413 let server = TcpListener::bind("127.0.0.1:0").await.unwrap();
414 let mut config = HealthConfig::new(server.local_addr().unwrap(), 1);
415 config.force_tcp = true;
416 let health = Health::with_config(config);
417 health.start();
418 let (_stream, _) = timeout(Duration::from_secs(1), server.accept())
419 .await
420 .unwrap()
421 .unwrap();
422 timeout(Duration::from_secs(2), async {
423 while health.fails() == 0 {
424 tokio::task::yield_now().await;
425 }
426 })
427 .await
428 .unwrap();
429 assert_eq!(health.fails(), 1);
430 assert!(!health.down());
431 }
432}