1use std::{
16 collections::{HashMap, VecDeque},
17 hash::{Hash, Hasher},
18 net::SocketAddr,
19 time::Instant,
20};
21
22use slab::Slab;
23
24use crate::protocol::udp::{
25 ClusterConfig, ConfigEvent, DropReason, FlowId, FlowKey, ManagerInput, MetricEvent, Output,
26 Transmit,
27 flow::{CloseReason, FlowPhase, UdpFlow},
28 proxy_protocol::prepend_dgram_header,
29};
30
31pub trait FlowKeyExtractor {
37 fn flow_key(&self, src: SocketAddr, payload: &[u8], cfg: &ClusterConfig) -> Option<FlowKey>;
41}
42
43#[derive(Clone, Copy, Debug, Default)]
46pub struct SourceTupleExtractor;
47
48impl FlowKeyExtractor for SourceTupleExtractor {
49 fn flow_key(&self, src: SocketAddr, payload: &[u8], cfg: &ClusterConfig) -> Option<FlowKey> {
50 if payload.is_empty() {
52 return None;
53 }
54 Some(FlowKey::from_src(src, cfg.affinity_with_port))
55 }
56}
57
58pub struct UdpManager<E: FlowKeyExtractor = SourceTupleExtractor> {
61 table: HashMap<FlowKey, FlowId>,
63 flows: Slab<UdpFlow>,
65 max_flows: usize,
67 max_rx_datagram_size: usize,
70 cluster: ClusterConfig,
72 hash_seed: u64,
75 extractor: E,
77 draining: bool,
79
80 outputs: VecDeque<Output>,
82 armed_deadline: Option<Instant>,
85
86 #[cfg(debug_assertions)]
93 max_flows_high_water: usize,
94}
95
96impl UdpManager<SourceTupleExtractor> {
97 pub fn new(
99 cluster: ClusterConfig,
100 max_flows: usize,
101 max_rx_datagram_size: usize,
102 hash_seed: u64,
103 ) -> Self {
104 Self::with_extractor(
105 cluster,
106 max_flows,
107 max_rx_datagram_size,
108 hash_seed,
109 SourceTupleExtractor,
110 )
111 }
112}
113
114impl<E: FlowKeyExtractor> UdpManager<E> {
115 pub fn with_extractor(
117 cluster: ClusterConfig,
118 max_flows: usize,
119 max_rx_datagram_size: usize,
120 hash_seed: u64,
121 extractor: E,
122 ) -> Self {
123 UdpManager {
124 table: HashMap::new(),
125 flows: Slab::new(),
126 max_flows,
127 max_rx_datagram_size,
128 cluster,
129 hash_seed,
130 extractor,
131 draining: false,
132 outputs: VecDeque::new(),
133 armed_deadline: None,
134 #[cfg(debug_assertions)]
135 max_flows_high_water: max_flows,
136 }
137 }
138
139 pub fn flow_count(&self) -> usize {
143 self.flows.len()
144 }
145
146 pub fn max_flows(&self) -> usize {
148 self.max_flows
149 }
150
151 pub fn is_draining(&self) -> bool {
153 self.draining
154 }
155
156 pub fn affinity_with_port(&self) -> bool {
160 self.cluster.affinity_with_port
161 }
162
163 pub fn flow(&self, flow: FlowId) -> Option<&UdpFlow> {
165 self.flows.get(flow)
166 }
167
168 pub fn handle_input(&mut self, input: ManagerInput<'_>, now: Instant) {
172 match input {
173 ManagerInput::ClientDatagram { src, payload } => {
174 self.on_client_datagram(src, payload, now)
175 }
176 ManagerInput::BackendDatagram { flow, payload } => {
177 self.on_backend_datagram(flow, payload, now)
178 }
179 ManagerInput::Config(event) => self.on_config(event, now),
180 ManagerInput::BackendResolved {
181 flow,
182 backend,
183 addr,
184 } => self.on_backend_resolved(flow, backend, addr, now),
185 }
186 self.debug_assert_invariants();
189 }
190
191 pub fn abort_flow(&mut self, flow: FlowId, _now: Instant, reason: CloseReason) {
206 self.close_flow(flow, reason);
207 self.debug_assert_invariants();
208 }
209
210 pub fn close_all(&mut self, now: Instant) {
222 let live: Vec<FlowId> = self.flows.iter().map(|(id, _)| id).collect();
226 for flow_id in live {
227 self.abort_flow(flow_id, now, CloseReason::Drain);
228 }
229 #[cfg(debug_assertions)]
231 {
232 debug_assert_eq!(self.flow_count(), 0, "close_all must drain every flow");
233 debug_assert!(
234 self.table.is_empty(),
235 "close_all must clear every table entry"
236 );
237 debug_assert!(
238 self.armed_deadline.is_none(),
239 "close_all must leave no armed timer"
240 );
241 }
242 self.debug_assert_invariants();
243 }
244
245 fn on_client_datagram(&mut self, src: SocketAddr, payload: &[u8], now: Instant) {
246 if payload.len() > self.max_rx_datagram_size {
248 self.drop_datagram(DropReason::Truncated);
249 return;
250 }
251 if self.cluster.cluster.is_empty() {
253 self.drop_datagram(DropReason::NoBackend);
254 return;
255 }
256 let key = match self.extractor.flow_key(src, payload, &self.cluster) {
258 Some(key) => key,
259 None => {
260 self.drop_datagram(DropReason::Invalid);
261 return;
262 }
263 };
264
265 if let Some(&flow_id) = self.table.get(&key) {
267 self.forward_on_existing_flow(flow_id, payload, now);
268 return;
269 }
270
271 let flows_before_admit = self.flows.len();
278 if self.draining {
279 self.drop_datagram(DropReason::Shed);
280 debug_assert_eq!(
281 self.flows.len(),
282 flows_before_admit,
283 "drain shed must allocate no flow"
284 );
285 return;
286 }
287 if self.flows.len() >= self.max_flows {
288 self.outputs
289 .push_back(Output::Metric(MetricEvent::FlowShed));
290 self.drop_datagram(DropReason::Shed);
291 debug_assert_eq!(
292 self.flows.len(),
293 flows_before_admit,
294 "cap shed must allocate no flow"
295 );
296 return;
297 }
298
299 debug_assert!(
305 self.flows.len() < self.max_flows,
306 "admit path entered while at/over the cap"
307 );
308 debug_assert!(
309 !self.table.contains_key(&key),
310 "admit path entered for a key already in the table (would orphan a flow)"
311 );
312
313 let mut flow = UdpFlow::new(src, self.cluster.clone(), now);
320 flow.pending_payload = Some(payload.to_vec());
321 let key_hash = self.affinity_hash(&flow);
322 let flow_id = self.flows.insert(flow);
323 self.table.insert(key, flow_id);
324
325 debug_assert_eq!(
328 self.flows.len(),
329 flows_before_admit + 1,
330 "admission must add exactly one flow"
331 );
332 debug_assert_eq!(
333 self.table.get(&key),
334 Some(&flow_id),
335 "admission must map the key to the new flow"
336 );
337
338 self.outputs
339 .push_back(Output::Metric(MetricEvent::FlowCreated));
340 self.outputs.push_back(Output::SelectBackend {
341 flow: flow_id,
342 cluster: self.cluster.cluster.clone(),
343 key: key_hash,
344 });
345 self.reschedule();
347 }
348
349 fn forward_on_existing_flow(&mut self, flow_id: FlowId, payload: &[u8], now: Instant) {
350 let Some(flow) = self.flows.get_mut(flow_id) else {
351 self.drop_datagram(DropReason::UnknownFlow);
353 return;
354 };
355
356 match flow.phase {
357 FlowPhase::AwaitingBackend => {
358 flow.pending_payload = Some(payload.to_vec());
364 flow.touch(flow.config.front_timeout, now);
365 self.reschedule();
366 }
367 FlowPhase::Established => {
368 flow.on_client_datagram(now);
369 let backend = flow
370 .backend_addr
371 .expect("Established flow always has a backend address");
372 let mut out = payload.to_vec();
373 if flow.take_proxy_protocol() {
374 prepend_dgram_header(&mut out, flow.client, backend);
375 }
376 let teardown = flow.teardown_reason();
377 self.outputs
378 .push_back(Output::Metric(MetricEvent::DatagramIn(payload.len())));
379 self.outputs.push_back(Output::SendToBackend(Transmit {
380 dst: backend,
381 segment_size: None,
382 payload: out,
383 }));
384 if let Some(reason) = teardown {
385 self.close_flow(flow_id, reason);
386 } else {
387 self.reschedule();
388 }
389 }
390 FlowPhase::Closing => {
391 self.drop_datagram(DropReason::Shed);
393 }
394 }
395 }
396
397 fn on_backend_resolved(
398 &mut self,
399 flow_id: FlowId,
400 backend: String,
401 addr: SocketAddr,
402 now: Instant,
403 ) {
404 let Some(flow) = self.flows.get_mut(flow_id) else {
405 self.drop_datagram(DropReason::UnknownFlow);
407 return;
408 };
409 if flow.phase != FlowPhase::AwaitingBackend {
410 return;
412 }
413 flow.backend_id = Some(backend);
414 flow.backend_addr = Some(addr);
415 flow.set_phase(FlowPhase::Established);
416
417 self.outputs.push_back(Output::OpenUpstream {
420 flow: flow_id,
421 backend: addr,
422 });
423
424 let pending = flow.pending_payload.take();
429 if let Some(mut payload) = pending {
430 let payload_len = payload.len();
431 flow.on_client_datagram(now);
432 if flow.take_proxy_protocol() {
433 prepend_dgram_header(&mut payload, flow.client, addr);
434 }
435 let teardown = flow.teardown_reason();
436 self.outputs
437 .push_back(Output::Metric(MetricEvent::DatagramIn(payload_len)));
438 self.outputs.push_back(Output::SendToBackend(Transmit {
439 dst: addr,
440 segment_size: None,
441 payload,
442 }));
443 if let Some(reason) = teardown {
444 self.close_flow(flow_id, reason);
445 return;
446 }
447 self.reschedule();
449 return;
450 }
451 let _gen = self
454 .flows
455 .get_mut(flow_id)
456 .map(|f| f.touch(self.cluster.front_timeout, now));
457 self.reschedule();
458 }
459
460 fn on_backend_datagram(&mut self, flow_id: FlowId, payload: &[u8], now: Instant) {
461 if payload.len() > self.max_rx_datagram_size {
462 self.drop_datagram(DropReason::Truncated);
463 return;
464 }
465 let Some(flow) = self.flows.get_mut(flow_id) else {
466 self.drop_datagram(DropReason::UnknownFlow);
467 return;
468 };
469 if flow.phase != FlowPhase::Established {
470 self.drop_datagram(DropReason::UnknownFlow);
471 return;
472 }
473 flow.on_backend_datagram(now);
474 let client = flow.client;
475 let teardown = flow.teardown_reason();
476 self.outputs
477 .push_back(Output::Metric(MetricEvent::DatagramOut(payload.len())));
478 self.outputs.push_back(Output::SendToClient(Transmit {
479 dst: client,
480 segment_size: None,
481 payload: payload.to_vec(),
482 }));
483 if let Some(reason) = teardown {
484 self.close_flow(flow_id, reason);
485 } else {
486 self.reschedule();
487 }
488 }
489
490 fn on_config(&mut self, event: ConfigEvent, _now: Instant) {
491 match event {
492 ConfigEvent::SetCluster(cfg) => self.cluster = cfg,
493 ConfigEvent::SetMaxFlows(n) => {
494 self.max_flows = n;
495 #[cfg(debug_assertions)]
499 {
500 self.max_flows_high_water = self.max_flows_high_water.max(n);
501 }
502 }
503 ConfigEvent::SetMaxRxDatagramSize(n) => self.max_rx_datagram_size = n,
504 ConfigEvent::Drain => self.draining = true,
505 }
506 }
507
508 pub fn handle_timeout(&mut self, now: Instant) {
515 let due: Vec<FlowId> = self
517 .flows
518 .iter()
519 .filter(|(_, flow)| flow.idle_deadline <= now)
520 .map(|(id, _)| id)
521 .collect();
522 for flow_id in due {
523 if let Some(flow) = self.flows.get(flow_id)
526 && flow.idle_deadline <= now
527 && flow.phase != FlowPhase::Closing
528 {
529 self.close_flow(flow_id, CloseReason::Idle);
530 }
531 }
532 self.reschedule();
533
534 #[cfg(debug_assertions)]
540 if let Some(next) = self.armed_deadline {
541 debug_assert!(
542 next > now,
543 "UdpManager::poll_timeout must strictly advance past a firing: \
544 armed {next:?} <= fired_at {now:?} (busy-loop)"
545 );
546 }
547
548 #[cfg(debug_assertions)]
554 for (id, flow) in self.flows.iter() {
555 debug_assert!(
556 flow.idle_deadline > now,
557 "FlowId {id} still due after handle_timeout: deadline {:?} <= now {now:?}",
558 flow.idle_deadline,
559 );
560 }
561 self.debug_assert_invariants();
562 }
563
564 pub fn poll_timeout(&self) -> Option<Instant> {
569 self.armed_deadline
570 }
571
572 pub fn poll_output(&mut self) -> Option<Output> {
574 self.outputs.pop_front()
575 }
576
577 fn reschedule(&mut self) {
582 let next = self
583 .flows
584 .iter()
585 .filter(|(_, f)| f.phase != FlowPhase::Closing)
586 .map(|(_, f)| f.idle_deadline)
587 .min();
588 if next != self.armed_deadline {
589 self.armed_deadline = next;
590 if let Some(deadline) = next {
591 self.outputs.push_back(Output::ArmTimer(deadline));
592 }
593 }
594 }
595
596 fn close_flow(&mut self, flow_id: FlowId, _reason: CloseReason) {
600 let Some(flow) = self.flows.get_mut(flow_id) else {
601 return;
602 };
603 if flow.phase == FlowPhase::Closing {
604 return;
605 }
606 flow.set_phase(FlowPhase::Closing);
607 let key = FlowKey::from_src(flow.client, self.cluster.affinity_with_port);
608 if self.table.get(&key) == Some(&flow_id) {
611 self.table.remove(&key);
612 } else {
613 let own_key = FlowKey::from_src(flow.client, flow.config.affinity_with_port);
616 if self.table.get(&own_key) == Some(&flow_id) {
617 self.table.remove(&own_key);
618 }
619 }
620 self.flows.remove(flow_id);
621 self.outputs
622 .push_back(Output::Metric(MetricEvent::FlowEvicted));
623 self.outputs.push_back(Output::CloseFlow(flow_id));
624 self.reschedule();
625
626 #[cfg(debug_assertions)]
631 {
632 debug_assert!(
633 !self.flows.contains(flow_id),
634 "close_flow left FlowId {flow_id} in the slab"
635 );
636 debug_assert!(
637 self.table.values().all(|&id| id != flow_id),
638 "close_flow left a table entry mapping to the removed FlowId {flow_id}"
639 );
640 }
641 }
642
643 fn drop_datagram(&mut self, reason: DropReason) {
646 let flows_before_drop = self.flows.len();
652 self.outputs
653 .push_back(Output::Metric(MetricEvent::DatagramDropped(reason)));
654 self.outputs.push_back(Output::Drop(reason));
655 debug_assert_eq!(
656 self.flows.len(),
657 flows_before_drop,
658 "a drop must allocate nothing and free nothing (flows.len() unchanged)"
659 );
660 }
661
662 fn affinity_hash(&self, flow: &UdpFlow) -> u64 {
665 let mut hasher = std::collections::hash_map::DefaultHasher::new();
666 self.hash_seed.hash(&mut hasher);
667 if flow.config.affinity_with_port {
668 flow.client.hash(&mut hasher);
669 } else {
670 flow.client.ip().hash(&mut hasher);
671 }
672 hasher.finish()
673 }
674
675 #[cfg(debug_assertions)]
686 fn check_invariants(&self) {
687 use std::collections::HashSet;
688
689 debug_assert_eq!(
691 self.flow_count(),
692 self.flows.len(),
693 "flow_count() must equal flows.len()"
694 );
695
696 let mut seen_ids: HashSet<FlowId> = HashSet::with_capacity(self.table.len());
699 for (key, &id) in self.table.iter() {
700 debug_assert!(
701 self.flows.contains(id),
702 "table key {key:?} maps to FlowId {id} absent from the slab (dangling key)"
703 );
704 debug_assert!(
705 seen_ids.insert(id),
706 "table injectivity violated: FlowId {id} is the target of two distinct FlowKeys"
707 );
708 }
709 debug_assert!(
712 seen_ids.len() <= self.flows.len(),
713 "more distinct table targets than live flows"
714 );
715
716 let mut min_live_deadline: Option<Instant> = None;
718 let mut live_count = 0usize;
719 for (id, flow) in self.flows.iter() {
720 debug_assert_ne!(
724 flow.phase,
725 FlowPhase::Closing,
726 "FlowId {id} persists in the slab while Closing (close_flow must remove it)"
727 );
728 debug_assert!(
729 matches!(
730 flow.phase,
731 FlowPhase::AwaitingBackend | FlowPhase::Established
732 ),
733 "FlowId {id} has an unexpected live phase {:?}",
734 flow.phase
735 );
736
737 match flow.phase {
740 FlowPhase::Established => debug_assert!(
741 flow.backend_addr.is_some(),
742 "Established FlowId {id} has no backend address"
743 ),
744 FlowPhase::AwaitingBackend => debug_assert!(
745 flow.backend_addr.is_none(),
746 "AwaitingBackend FlowId {id} already carries a backend address"
747 ),
748 FlowPhase::Closing => {}
749 }
750
751 if flow.requests_exhausted() || flow.responses_exhausted() {
755 debug_assert!(
756 flow.teardown_reason().is_some(),
757 "FlowId {id} exhausted a cap (req {}/{}, resp {}/{}) but reports no teardown",
758 flow.requests_seen,
759 flow.config.requests,
760 flow.responses_seen,
761 flow.config.responses,
762 );
763 } else {
764 debug_assert!(
768 flow.teardown_reason().is_none(),
769 "FlowId {id} reports a teardown while within both caps"
770 );
771 }
772
773 live_count += 1;
774 min_live_deadline = Some(match min_live_deadline {
775 Some(d) => d.min(flow.idle_deadline),
776 None => flow.idle_deadline,
777 });
778 }
779
780 debug_assert!(
784 self.flows.len() <= self.max_flows_high_water,
785 "live flows {} exceed the high-water cap {}",
786 self.flows.len(),
787 self.max_flows_high_water,
788 );
789
790 debug_assert_eq!(
795 self.armed_deadline.is_some(),
796 live_count > 0,
797 "timer coherence: armed_deadline.is_some() ({}) must match having live flows ({})",
798 self.armed_deadline.is_some(),
799 live_count > 0,
800 );
801 if let Some(armed) = self.armed_deadline {
802 debug_assert_eq!(
803 Some(armed),
804 min_live_deadline,
805 "timer coherence: armed deadline {armed:?} must equal the minimum live idle deadline {min_live_deadline:?}"
806 );
807 }
808 }
809
810 #[inline]
814 fn debug_assert_invariants(&self) {
815 #[cfg(debug_assertions)]
816 self.check_invariants();
817 }
818}
819
820#[cfg(test)]
821mod tests {
822 use std::{
823 net::{IpAddr, Ipv4Addr},
824 time::Duration,
825 };
826
827 use super::*;
828
829 fn cluster(name: &str) -> ClusterConfig {
830 ClusterConfig {
831 cluster: name.to_owned(),
832 front_timeout: Duration::from_secs(30),
833 back_timeout: Duration::from_secs(30),
834 ..Default::default()
835 }
836 }
837
838 fn client(n: u8, port: u16) -> SocketAddr {
839 SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, n)), port)
840 }
841
842 fn backend() -> SocketAddr {
843 SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 5300)
844 }
845
846 fn drain(mgr: &mut UdpManager) -> Vec<Output> {
848 let mut out = Vec::new();
849 while let Some(o) = mgr.poll_output() {
850 out.push(o);
851 }
852 out
853 }
854
855 #[test]
856 fn unknown_datagram_allocates_nothing_when_no_cluster() {
857 let mut mgr = UdpManager::new(ClusterConfig::default(), 16, 65535, 0xABCD);
858 let now = Instant::now();
859 mgr.handle_input(
860 ManagerInput::ClientDatagram {
861 src: client(1, 1000),
862 payload: b"hi",
863 },
864 now,
865 );
866 assert_eq!(mgr.flow_count(), 0);
867 let outs = drain(&mut mgr);
868 assert!(
869 outs.iter()
870 .any(|o| matches!(o, Output::Drop(DropReason::NoBackend)))
871 );
872 assert!(
873 !outs
874 .iter()
875 .any(|o| matches!(o, Output::SelectBackend { .. }))
876 );
877 }
878
879 #[test]
880 fn empty_datagram_is_invalid() {
881 let mut mgr = UdpManager::new(cluster("dns"), 16, 65535, 1);
882 mgr.handle_input(
883 ManagerInput::ClientDatagram {
884 src: client(1, 1000),
885 payload: b"",
886 },
887 Instant::now(),
888 );
889 assert_eq!(mgr.flow_count(), 0);
890 let outs = drain(&mut mgr);
891 assert!(
892 outs.iter()
893 .any(|o| matches!(o, Output::Drop(DropReason::Invalid)))
894 );
895 }
896
897 #[test]
898 fn truncated_datagram_dropped_before_admission() {
899 let mut mgr = UdpManager::new(cluster("dns"), 16, 4, 1);
900 mgr.handle_input(
901 ManagerInput::ClientDatagram {
902 src: client(1, 1000),
903 payload: b"toolong",
904 },
905 Instant::now(),
906 );
907 assert_eq!(mgr.flow_count(), 0);
908 let outs = drain(&mut mgr);
909 assert!(
910 outs.iter()
911 .any(|o| matches!(o, Output::Drop(DropReason::Truncated)))
912 );
913 }
914
915 #[test]
916 fn new_flow_requests_backend_then_forwards_buffered_datagram() {
917 let mut mgr = UdpManager::new(cluster("dns"), 16, 65535, 7);
918 let now = Instant::now();
919 let src = client(1, 1000);
920 mgr.handle_input(
921 ManagerInput::ClientDatagram {
922 src,
923 payload: b"query",
924 },
925 now,
926 );
927 assert_eq!(mgr.flow_count(), 1);
928 let outs = drain(&mut mgr);
929 let select = outs
930 .iter()
931 .find_map(|o| match o {
932 Output::SelectBackend { flow, cluster, .. } => Some((*flow, cluster.clone())),
933 _ => None,
934 })
935 .expect("SelectBackend emitted");
936 assert_eq!(select.1, "dns");
937 assert!(!outs.iter().any(|o| matches!(o, Output::SendToBackend(_))));
939
940 mgr.handle_input(
941 ManagerInput::BackendResolved {
942 flow: select.0,
943 backend: "b1".to_owned(),
944 addr: backend(),
945 },
946 now,
947 );
948 let outs = drain(&mut mgr);
949 assert!(
950 outs.iter()
951 .any(|o| matches!(o, Output::OpenUpstream { .. }))
952 );
953 let sent = outs
954 .iter()
955 .find_map(|o| match o {
956 Output::SendToBackend(t) => Some(t.clone()),
957 _ => None,
958 })
959 .expect("buffered datagram flushed on resolve");
960 assert_eq!(sent.dst, backend());
961 assert_eq!(sent.payload, b"query");
962 }
963
964 #[test]
965 fn tracked_flow_reuses_backend() {
966 let mut mgr = UdpManager::new(cluster("dns"), 16, 65535, 7);
967 let now = Instant::now();
968 let src = client(1, 1000);
969 mgr.handle_input(
970 ManagerInput::ClientDatagram {
971 src,
972 payload: b"q1",
973 },
974 now,
975 );
976 let select_flow = drain(&mut mgr)
977 .into_iter()
978 .find_map(|o| match o {
979 Output::SelectBackend { flow, .. } => Some(flow),
980 _ => None,
981 })
982 .unwrap();
983 mgr.handle_input(
984 ManagerInput::BackendResolved {
985 flow: select_flow,
986 backend: "b1".to_owned(),
987 addr: backend(),
988 },
989 now,
990 );
991 drain(&mut mgr);
992
993 mgr.handle_input(
995 ManagerInput::ClientDatagram {
996 src,
997 payload: b"q2",
998 },
999 now,
1000 );
1001 assert_eq!(mgr.flow_count(), 1);
1002 let outs = drain(&mut mgr);
1003 assert!(
1004 !outs
1005 .iter()
1006 .any(|o| matches!(o, Output::SelectBackend { .. }))
1007 );
1008 assert!(
1009 outs.iter()
1010 .any(|o| matches!(o, Output::SendToBackend(t) if t.payload == b"q2"))
1011 );
1012 }
1013
1014 #[test]
1015 fn responses_one_closes_flow_after_single_reply() {
1016 let mut cfg = cluster("dns");
1017 cfg.responses = 1;
1018 let mut mgr = UdpManager::new(cfg, 16, 65535, 7);
1019 let now = Instant::now();
1020 let src = client(1, 1000);
1021 mgr.handle_input(ManagerInput::ClientDatagram { src, payload: b"q" }, now);
1022 let flow = drain(&mut mgr)
1023 .into_iter()
1024 .find_map(|o| match o {
1025 Output::SelectBackend { flow, .. } => Some(flow),
1026 _ => None,
1027 })
1028 .unwrap();
1029 mgr.handle_input(
1030 ManagerInput::BackendResolved {
1031 flow,
1032 backend: "b1".to_owned(),
1033 addr: backend(),
1034 },
1035 now,
1036 );
1037 drain(&mut mgr);
1038
1039 mgr.handle_input(
1041 ManagerInput::BackendDatagram {
1042 flow,
1043 payload: b"answer",
1044 },
1045 now,
1046 );
1047 let outs = drain(&mut mgr);
1048 assert!(
1049 outs.iter()
1050 .any(|o| matches!(o, Output::SendToClient(t) if t.payload == b"answer"))
1051 );
1052 assert!(
1053 outs.iter()
1054 .any(|o| matches!(o, Output::CloseFlow(f) if *f == flow))
1055 );
1056 assert_eq!(mgr.flow_count(), 0);
1057 }
1058
1059 #[test]
1060 fn requests_cap_closes_flow() {
1061 let mut cfg = cluster("syslog");
1062 cfg.requests = 2;
1063 let mut mgr = UdpManager::new(cfg, 16, 65535, 7);
1064 let now = Instant::now();
1065 let src = client(1, 1000);
1066 mgr.handle_input(ManagerInput::ClientDatagram { src, payload: b"1" }, now);
1067 let flow = drain(&mut mgr)
1068 .into_iter()
1069 .find_map(|o| match o {
1070 Output::SelectBackend { flow, .. } => Some(flow),
1071 _ => None,
1072 })
1073 .unwrap();
1074 mgr.handle_input(
1075 ManagerInput::BackendResolved {
1076 flow,
1077 backend: "b1".to_owned(),
1078 addr: backend(),
1079 },
1080 now,
1081 );
1082 drain(&mut mgr);
1083 mgr.handle_input(ManagerInput::ClientDatagram { src, payload: b"2" }, now);
1085 let outs = drain(&mut mgr);
1086 assert!(outs.iter().any(|o| matches!(o, Output::CloseFlow(_))));
1087 assert_eq!(mgr.flow_count(), 0);
1088 }
1089
1090 #[test]
1091 fn flow_table_full_sheds_new_flow() {
1092 let mut mgr = UdpManager::new(cluster("dns"), 1, 65535, 7);
1093 let now = Instant::now();
1094 mgr.handle_input(
1095 ManagerInput::ClientDatagram {
1096 src: client(1, 1000),
1097 payload: b"a",
1098 },
1099 now,
1100 );
1101 assert_eq!(mgr.flow_count(), 1);
1102 drain(&mut mgr);
1103 mgr.handle_input(
1105 ManagerInput::ClientDatagram {
1106 src: client(2, 1000),
1107 payload: b"b",
1108 },
1109 now,
1110 );
1111 assert_eq!(mgr.flow_count(), 1);
1112 let outs = drain(&mut mgr);
1113 assert!(
1114 outs.iter()
1115 .any(|o| matches!(o, Output::Metric(MetricEvent::FlowShed)))
1116 );
1117 assert!(
1118 outs.iter()
1119 .any(|o| matches!(o, Output::Drop(DropReason::Shed)))
1120 );
1121 }
1122
1123 #[test]
1124 fn idle_timeout_closes_flow() {
1125 let mut cfg = cluster("dns");
1126 cfg.front_timeout = Duration::from_secs(10);
1127 let mut mgr = UdpManager::new(cfg, 16, 65535, 7);
1128 let now = Instant::now();
1129 mgr.handle_input(
1130 ManagerInput::ClientDatagram {
1131 src: client(1, 1000),
1132 payload: b"q",
1133 },
1134 now,
1135 );
1136 drain(&mut mgr);
1137 let deadline = mgr.poll_timeout().expect("timer armed");
1138 assert!(deadline >= now + Duration::from_secs(10));
1139 mgr.handle_timeout(now + Duration::from_secs(11));
1141 let outs = drain(&mut mgr);
1142 assert!(outs.iter().any(|o| matches!(o, Output::CloseFlow(_))));
1143 assert_eq!(mgr.flow_count(), 0);
1144 assert!(mgr.poll_timeout().is_none());
1145 }
1146
1147 #[test]
1148 fn idle_race_resolved_by_generation_token() {
1149 let mut cfg = cluster("dns");
1151 cfg.front_timeout = Duration::from_secs(10);
1152 let mut mgr = UdpManager::new(cfg, 16, 65535, 7);
1153 let now = Instant::now();
1154 let src = client(1, 1000);
1155 mgr.handle_input(ManagerInput::ClientDatagram { src, payload: b"q" }, now);
1156 let flow = drain(&mut mgr)
1157 .into_iter()
1158 .find_map(|o| match o {
1159 Output::SelectBackend { flow, .. } => Some(flow),
1160 _ => None,
1161 })
1162 .unwrap();
1163 mgr.handle_input(
1164 ManagerInput::BackendResolved {
1165 flow,
1166 backend: "b1".to_owned(),
1167 addr: backend(),
1168 },
1169 now,
1170 );
1171 drain(&mut mgr);
1172 let gen0 = mgr.flow(flow).unwrap().timer_gen;
1173 let t5 = now + Duration::from_secs(5);
1175 mgr.handle_input(
1176 ManagerInput::ClientDatagram {
1177 src,
1178 payload: b"q2",
1179 },
1180 t5,
1181 );
1182 drain(&mut mgr);
1183 let gen1 = mgr.flow(flow).unwrap().timer_gen;
1184 assert_ne!(gen0, gen1, "generation token must bump on touch");
1185 mgr.handle_timeout(now + Duration::from_secs(10));
1188 assert_eq!(mgr.flow_count(), 1, "refreshed flow survives stale expiry");
1189 mgr.handle_timeout(now + Duration::from_secs(16));
1191 assert_eq!(mgr.flow_count(), 0);
1192 }
1193
1194 #[test]
1195 fn drain_sheds_new_flows_but_keeps_existing() {
1196 let mut mgr = UdpManager::new(cluster("dns"), 16, 65535, 7);
1197 let now = Instant::now();
1198 let src = client(1, 1000);
1199 mgr.handle_input(ManagerInput::ClientDatagram { src, payload: b"q" }, now);
1200 drain(&mut mgr);
1201 mgr.handle_input(ManagerInput::Config(ConfigEvent::Drain), now);
1202 mgr.handle_input(
1204 ManagerInput::ClientDatagram {
1205 src: client(2, 1000),
1206 payload: b"q",
1207 },
1208 now,
1209 );
1210 assert_eq!(mgr.flow_count(), 1, "existing flow kept, new flow shed");
1211 let outs = drain(&mut mgr);
1212 assert!(
1213 outs.iter()
1214 .any(|o| matches!(o, Output::Drop(DropReason::Shed)))
1215 );
1216 }
1217
1218 #[test]
1219 fn reconfig_midflow_preserves_existing_flow_contract() {
1220 let mut cfg = cluster("dns");
1221 cfg.responses = 0;
1222 let mut mgr = UdpManager::new(cfg, 16, 65535, 7);
1223 let now = Instant::now();
1224 let src = client(1, 1000);
1225 mgr.handle_input(ManagerInput::ClientDatagram { src, payload: b"q" }, now);
1226 let flow = drain(&mut mgr)
1227 .into_iter()
1228 .find_map(|o| match o {
1229 Output::SelectBackend { flow, .. } => Some(flow),
1230 _ => None,
1231 })
1232 .unwrap();
1233 mgr.handle_input(
1234 ManagerInput::BackendResolved {
1235 flow,
1236 backend: "b1".to_owned(),
1237 addr: backend(),
1238 },
1239 now,
1240 );
1241 drain(&mut mgr);
1242 let mut newcfg = cluster("dns");
1244 newcfg.responses = 1;
1245 mgr.handle_input(ManagerInput::Config(ConfigEvent::SetCluster(newcfg)), now);
1246 mgr.handle_input(
1248 ManagerInput::BackendDatagram {
1249 flow,
1250 payload: b"reply",
1251 },
1252 now,
1253 );
1254 assert_eq!(mgr.flow_count(), 1);
1255 }
1256
1257 #[test]
1258 fn reaper_drains_active_flows_to_zero() {
1259 let mut cfg = cluster("dns");
1260 cfg.front_timeout = Duration::from_secs(5);
1261 let mut mgr = UdpManager::new(cfg, 64, 65535, 7);
1262 let now = Instant::now();
1263 for n in 1..=10u8 {
1264 mgr.handle_input(
1265 ManagerInput::ClientDatagram {
1266 src: client(n, 1000),
1267 payload: b"q",
1268 },
1269 now,
1270 );
1271 }
1272 assert_eq!(mgr.flow_count(), 10);
1273 drain(&mut mgr);
1274 mgr.handle_timeout(now + Duration::from_secs(6));
1276 assert_eq!(mgr.flow_count(), 0, "reaper drains every flow, no leak");
1277 let outs = drain(&mut mgr);
1278 let closes = outs
1279 .iter()
1280 .filter(|o| matches!(o, Output::CloseFlow(_)))
1281 .count();
1282 assert_eq!(closes, 10);
1283 assert!(mgr.poll_timeout().is_none(), "no armed timer after drain");
1284 }
1285
1286 #[test]
1287 fn proxy_protocol_first_datagram_only() {
1288 let mut cfg = cluster("dns");
1289 cfg.send_proxy_protocol = true;
1290 cfg.proxy_protocol_every_datagram = false;
1291 let mut mgr = UdpManager::new(cfg, 16, 65535, 7);
1292 let now = Instant::now();
1293 let src = client(1, 1000);
1294 mgr.handle_input(
1295 ManagerInput::ClientDatagram {
1296 src,
1297 payload: b"q1",
1298 },
1299 now,
1300 );
1301 let flow = drain(&mut mgr)
1302 .into_iter()
1303 .find_map(|o| match o {
1304 Output::SelectBackend { flow, .. } => Some(flow),
1305 _ => None,
1306 })
1307 .unwrap();
1308 mgr.handle_input(
1309 ManagerInput::BackendResolved {
1310 flow,
1311 backend: "b1".to_owned(),
1312 addr: backend(),
1313 },
1314 now,
1315 );
1316 let first = drain(&mut mgr)
1318 .into_iter()
1319 .find_map(|o| match o {
1320 Output::SendToBackend(t) => Some(t.payload),
1321 _ => None,
1322 })
1323 .unwrap();
1324 assert!(first.len() > 2, "PPv2 header prepended to first datagram");
1325 assert_eq!(&first[..4], &[0x0D, 0x0A, 0x0D, 0x0A]);
1326 assert_eq!(first[12], 0x21);
1327 assert_eq!(first[13], 0x12);
1328 assert_eq!(&first[first.len() - 2..], b"q1");
1329
1330 mgr.handle_input(
1332 ManagerInput::ClientDatagram {
1333 src,
1334 payload: b"q2",
1335 },
1336 now,
1337 );
1338 let second = drain(&mut mgr)
1339 .into_iter()
1340 .find_map(|o| match o {
1341 Output::SendToBackend(t) => Some(t.payload),
1342 _ => None,
1343 })
1344 .unwrap();
1345 assert_eq!(second, b"q2", "no PPv2 prefix on subsequent datagrams");
1346 }
1347
1348 fn establish(mgr: &mut UdpManager, src: SocketAddr, now: Instant) -> FlowId {
1352 mgr.handle_input(ManagerInput::ClientDatagram { src, payload: b"q" }, now);
1353 let flow = drain(mgr)
1354 .into_iter()
1355 .find_map(|o| match o {
1356 Output::SelectBackend { flow, .. } => Some(flow),
1357 _ => None,
1358 })
1359 .unwrap();
1360 mgr.handle_input(
1361 ManagerInput::BackendResolved {
1362 flow,
1363 backend: "b1".to_owned(),
1364 addr: backend(),
1365 },
1366 now,
1367 );
1368 drain(mgr);
1369 flow
1370 }
1371
1372 #[test]
1373 fn close_all_evicts_every_flow_exactly_once() {
1374 let mut cfg = cluster("dns");
1375 cfg.front_timeout = Duration::from_secs(30);
1376 let mut mgr = UdpManager::new(cfg, 64, 65535, 7);
1377 let now = Instant::now();
1378 const N: usize = 6;
1380 let mut flow_ids = Vec::new();
1381 for n in 1..=4u8 {
1382 flow_ids.push(establish(&mut mgr, client(n, 1000), now));
1383 }
1384 for n in 5..=6u8 {
1386 mgr.handle_input(
1387 ManagerInput::ClientDatagram {
1388 src: client(n, 1000),
1389 payload: b"q",
1390 },
1391 now,
1392 );
1393 let flow = drain(&mut mgr)
1394 .into_iter()
1395 .find_map(|o| match o {
1396 Output::SelectBackend { flow, .. } => Some(flow),
1397 _ => None,
1398 })
1399 .unwrap();
1400 flow_ids.push(flow);
1401 }
1402 assert_eq!(mgr.flow_count(), N);
1403
1404 mgr.close_all(now);
1405 let outs = drain(&mut mgr);
1406 let evicted = outs
1407 .iter()
1408 .filter(|o| matches!(o, Output::Metric(MetricEvent::FlowEvicted)))
1409 .count();
1410 let closed = outs
1411 .iter()
1412 .filter(|o| matches!(o, Output::CloseFlow(_)))
1413 .count();
1414 assert_eq!(evicted, N, "one FlowEvicted per live flow");
1415 assert_eq!(closed, N, "one CloseFlow per live flow");
1416 assert_eq!(mgr.flow_count(), 0, "flow table + slab drained to zero");
1417 assert!(
1418 mgr.poll_timeout().is_none(),
1419 "no armed timer after close_all"
1420 );
1421 }
1422
1423 #[test]
1424 fn close_all_is_idempotent_and_empty_safe() {
1425 let mut mgr = UdpManager::new(cluster("dns"), 16, 65535, 7);
1426 let now = Instant::now();
1427 mgr.close_all(now);
1429 assert!(drain(&mut mgr).is_empty());
1430 assert_eq!(mgr.flow_count(), 0);
1431
1432 establish(&mut mgr, client(1, 1000), now);
1435 assert_eq!(mgr.flow_count(), 1);
1436 mgr.close_all(now);
1437 let first = drain(&mut mgr);
1438 assert_eq!(
1439 first
1440 .iter()
1441 .filter(|o| matches!(o, Output::CloseFlow(_)))
1442 .count(),
1443 1
1444 );
1445 mgr.close_all(now);
1446 assert!(drain(&mut mgr).is_empty(), "second close_all emits nothing");
1447 assert_eq!(mgr.flow_count(), 0);
1448 }
1449
1450 #[test]
1451 fn abort_flow_closes_established_flow() {
1452 let mut mgr = UdpManager::new(cluster("dns"), 16, 65535, 7);
1453 let now = Instant::now();
1454 let flow = establish(&mut mgr, client(1, 1000), now);
1455 assert_eq!(mgr.flow_count(), 1);
1456
1457 mgr.abort_flow(flow, now, CloseReason::Aborted);
1458 let outs = drain(&mut mgr);
1459 assert!(
1460 outs.iter()
1461 .any(|o| matches!(o, Output::Metric(MetricEvent::FlowEvicted)))
1462 );
1463 assert!(
1464 outs.iter()
1465 .any(|o| matches!(o, Output::CloseFlow(f) if *f == flow))
1466 );
1467 assert_eq!(mgr.flow_count(), 0);
1468
1469 mgr.abort_flow(flow, now, CloseReason::Aborted);
1471 assert!(drain(&mut mgr).is_empty());
1472 assert_eq!(mgr.flow_count(), 0);
1473 }
1474
1475 #[test]
1476 fn abort_flow_closes_awaiting_backend_flow() {
1477 let mut mgr = UdpManager::new(cluster("dns"), 16, 65535, 7);
1480 let now = Instant::now();
1481 mgr.handle_input(
1482 ManagerInput::ClientDatagram {
1483 src: client(1, 1000),
1484 payload: b"q",
1485 },
1486 now,
1487 );
1488 let flow = drain(&mut mgr)
1489 .into_iter()
1490 .find_map(|o| match o {
1491 Output::SelectBackend { flow, .. } => Some(flow),
1492 _ => None,
1493 })
1494 .unwrap();
1495 assert_eq!(mgr.flow_count(), 1);
1496 assert_eq!(mgr.flow(flow).unwrap().phase, FlowPhase::AwaitingBackend);
1497
1498 mgr.abort_flow(flow, now, CloseReason::Aborted);
1499 let outs = drain(&mut mgr);
1500 assert!(
1501 outs.iter()
1502 .any(|o| matches!(o, Output::Metric(MetricEvent::FlowEvicted)))
1503 );
1504 assert!(
1505 outs.iter()
1506 .any(|o| matches!(o, Output::CloseFlow(f) if *f == flow))
1507 );
1508 assert_eq!(mgr.flow_count(), 0, "AwaitingBackend slot freed by abort");
1509 assert!(mgr.poll_timeout().is_none());
1510 mgr.handle_input(
1513 ManagerInput::ClientDatagram {
1514 src: client(1, 1000),
1515 payload: b"q2",
1516 },
1517 now,
1518 );
1519 assert_eq!(mgr.flow_count(), 1);
1520 }
1521
1522 #[test]
1523 fn abort_flow_unknown_id_is_noop() {
1524 let mut mgr = UdpManager::new(cluster("dns"), 16, 65535, 7);
1525 let now = Instant::now();
1526 mgr.abort_flow(999, now, CloseReason::Aborted);
1527 assert!(drain(&mut mgr).is_empty());
1528 assert_eq!(mgr.flow_count(), 0);
1529 }
1530
1531 #[test]
1532 fn requests_counts_forwards_not_buffered_during_await() {
1533 let mut cfg = cluster("syslog");
1539 cfg.requests = 2;
1540 let mut mgr = UdpManager::new(cfg, 16, 65535, 7);
1541 let now = Instant::now();
1542 let src = client(1, 1000);
1543
1544 mgr.handle_input(ManagerInput::ClientDatagram { src, payload: b"1" }, now);
1546 let flow = drain(&mut mgr)
1547 .into_iter()
1548 .find_map(|o| match o {
1549 Output::SelectBackend { flow, .. } => Some(flow),
1550 _ => None,
1551 })
1552 .unwrap();
1553 for p in [b"2".as_slice(), b"3", b"4"] {
1555 mgr.handle_input(ManagerInput::ClientDatagram { src, payload: p }, now);
1556 }
1557 drain(&mut mgr);
1558 assert_eq!(mgr.flow_count(), 1, "await burst must not close the flow");
1560 assert_eq!(
1561 mgr.flow(flow).unwrap().requests_seen,
1562 0,
1563 "buffered-only datagrams must not count toward requests"
1564 );
1565
1566 mgr.handle_input(
1568 ManagerInput::BackendResolved {
1569 flow,
1570 backend: "b1".to_owned(),
1571 addr: backend(),
1572 },
1573 now,
1574 );
1575 let outs = drain(&mut mgr);
1576 assert!(
1577 outs.iter()
1578 .any(|o| matches!(o, Output::SendToBackend(t) if t.payload == b"4")),
1579 "newest buffered datagram is the one forwarded"
1580 );
1581 assert!(
1582 !outs.iter().any(|o| matches!(o, Output::CloseFlow(_))),
1583 "one forward must not reach requests=2"
1584 );
1585 assert_eq!(mgr.flow_count(), 1);
1586 assert_eq!(mgr.flow(flow).unwrap().requests_seen, 1);
1587
1588 mgr.handle_input(ManagerInput::ClientDatagram { src, payload: b"5" }, now);
1590 let outs = drain(&mut mgr);
1591 assert!(
1592 outs.iter()
1593 .any(|o| matches!(o, Output::SendToBackend(t) if t.payload == b"5"))
1594 );
1595 assert!(
1596 outs.iter().any(|o| matches!(o, Output::CloseFlow(_))),
1597 "second real forward reaches requests=2"
1598 );
1599 assert_eq!(mgr.flow_count(), 0);
1600 }
1601
1602 use quickcheck::{Arbitrary, Gen, quickcheck};
1605
1606 #[derive(Clone, Debug)]
1609 enum Step {
1610 Client(u8),
1612 Tick(u8),
1614 }
1615
1616 impl Arbitrary for Step {
1617 fn arbitrary(g: &mut Gen) -> Self {
1618 if bool::arbitrary(g) {
1619 Step::Client(u8::arbitrary(g))
1620 } else {
1621 Step::Tick(u8::arbitrary(g) % 40)
1622 }
1623 }
1624 }
1625
1626 #[test]
1633 fn prop_flow_invariants() {
1634 fn prop(steps: Vec<Step>) -> bool {
1635 const MAX_FLOWS: usize = 4;
1636 let mut cfg = cluster("dns");
1637 cfg.front_timeout = Duration::from_secs(10);
1638 let mut mgr = UdpManager::new(cfg, MAX_FLOWS, 65535, 0x5EED);
1639 let base = Instant::now();
1640 let mut now = base;
1641 let mut created = 0usize;
1642 let mut closed = 0usize;
1643
1644 for step in steps {
1645 match step {
1646 Step::Client(id) => {
1647 let src = client(id % 8, 9000 + (id % 8) as u16);
1648 mgr.handle_input(ManagerInput::ClientDatagram { src, payload: b"q" }, now);
1649 }
1650 Step::Tick(secs) => {
1651 now += Duration::from_secs(secs as u64);
1652 mgr.handle_timeout(now);
1653 }
1654 }
1655 while let Some(out) = mgr.poll_output() {
1657 match out {
1658 Output::Metric(MetricEvent::FlowCreated) => created += 1,
1659 Output::CloseFlow(_) => closed += 1,
1660 _ => {}
1661 }
1662 }
1663 if mgr.flow_count() > MAX_FLOWS {
1665 return false;
1666 }
1667 }
1668
1669 now += Duration::from_secs(60);
1671 mgr.handle_timeout(now);
1672 while let Some(out) = mgr.poll_output() {
1673 if let Output::CloseFlow(_) = out {
1674 closed += 1;
1675 }
1676 }
1677 mgr.flow_count() == 0 && mgr.poll_timeout().is_none() && created == closed
1678 }
1679 quickcheck(prop as fn(Vec<Step>) -> bool);
1680 }
1681
1682 #[test]
1687 fn prop_generation_token_defeats_stale_close() {
1688 fn prop(refresh_offset: u8) -> bool {
1689 let timeout = 20u64;
1690 let offset = 1 + (refresh_offset as u64 % (timeout - 1));
1692 let mut cfg = cluster("dns");
1693 cfg.front_timeout = Duration::from_secs(timeout);
1694 let mut mgr = UdpManager::new(cfg, 8, 65535, 1);
1695 let now = Instant::now();
1696 let src = client(1, 1000);
1697 mgr.handle_input(ManagerInput::ClientDatagram { src, payload: b"q" }, now);
1698 while mgr.poll_output().is_some() {}
1699
1700 let refreshed_at = now + Duration::from_secs(offset);
1703 mgr.handle_input(
1704 ManagerInput::ClientDatagram {
1705 src,
1706 payload: b"q2",
1707 },
1708 refreshed_at,
1709 );
1710 while mgr.poll_output().is_some() {}
1711
1712 mgr.handle_timeout(now + Duration::from_secs(timeout));
1714 while mgr.poll_output().is_some() {}
1715 if mgr.flow_count() != 1 {
1716 return false;
1717 }
1718 mgr.handle_timeout(refreshed_at + Duration::from_secs(timeout + 1));
1720 while mgr.poll_output().is_some() {}
1721 mgr.flow_count() == 0
1722 }
1723 quickcheck(prop as fn(u8) -> bool);
1724 }
1725}