1use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, TcpListener, UdpSocket};
6use std::time::Duration;
7
8use isoloom_manifest::Protocol;
9
10use crate::BoxFuture;
11
12pub struct HeldPort {
14 pub port: u16,
16 _hold: Box<dyn Send + Sync>,
17}
18
19impl std::fmt::Debug for HeldPort {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 f.debug_struct("HeldPort").field("port", &self.port).finish_non_exhaustive()
22 }
23}
24
25impl HeldPort {
26 pub fn new(port: u16, hold: impl Send + Sync + 'static) -> HeldPort {
28 HeldPort { port, _hold: Box::new(hold) }
29 }
30}
31
32pub trait HostPorts: Send + Sync {
34 fn in_use(&self, port: u16, protocol: Protocol) -> bool;
36 fn pick_free(&self, protocol: Protocol) -> Option<HeldPort>;
38 fn reachable<'a>(&'a self, host: &'a str, port: u16, within: Duration) -> BoxFuture<'a, bool>;
40}
41
42#[derive(Debug, Clone, Copy, Default)]
45pub struct LoopbackPorts;
46
47impl HostPorts for LoopbackPorts {
48 fn in_use(&self, port: u16, protocol: Protocol) -> bool {
51 let v4 = bind(Ipv4Addr::LOCALHOST.into(), port, protocol);
52 let v6 = bind(Ipv6Addr::LOCALHOST.into(), port, protocol);
53 v4.is_err() || v6.is_err_and(|e| held_elsewhere(&e))
54 }
55
56 fn pick_free(&self, protocol: Protocol) -> Option<HeldPort> {
57 match protocol {
58 Protocol::Udp => {
59 let s = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).ok()?;
60 Some(HeldPort::new(s.local_addr().ok()?.port(), s))
61 }
62 Protocol::Tcp => {
63 let l = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).ok()?;
64 Some(HeldPort::new(l.local_addr().ok()?.port(), l))
65 }
66 }
67 }
68
69 fn reachable<'a>(&'a self, host: &'a str, port: u16, within: Duration) -> BoxFuture<'a, bool> {
70 Box::pin(async move { matches!(tokio::time::timeout(within, tokio::net::TcpStream::connect((host, port))).await, Ok(Ok(_))) })
71 }
72}
73
74fn bind(ip: IpAddr, port: u16, protocol: Protocol) -> std::io::Result<()> {
76 match protocol {
77 Protocol::Udp => UdpSocket::bind((ip, port)).map(drop),
78 Protocol::Tcp => TcpListener::bind((ip, port)).map(drop),
79 }
80}
81
82fn held_elsewhere(e: &std::io::Error) -> bool {
85 matches!(e.kind(), std::io::ErrorKind::AddrInUse | std::io::ErrorKind::PermissionDenied)
86}
87
88#[cfg(any(test, feature = "fake"))]
91#[derive(Debug, Default)]
92pub struct FakePorts {
93 taken: std::sync::Mutex<std::collections::BTreeSet<(u16, Protocol)>>,
94 free: std::sync::Mutex<std::collections::VecDeque<u16>>,
95 answering: std::sync::Mutex<std::collections::BTreeSet<(String, u16)>>,
96}
97
98#[cfg(any(test, feature = "fake"))]
99impl FakePorts {
100 pub fn new(free: impl IntoIterator<Item = u16>) -> FakePorts {
102 FakePorts {
103 free: std::sync::Mutex::new(free.into_iter().collect()),
104 ..FakePorts::default()
105 }
106 }
107
108 pub fn take(&self, port: u16, protocol: Protocol) {
110 self.taken.lock().unwrap_or_else(std::sync::PoisonError::into_inner).insert((port, protocol));
111 }
112
113 pub fn answer(&self, host: &str, port: u16) {
115 self.answering
116 .lock()
117 .unwrap_or_else(std::sync::PoisonError::into_inner)
118 .insert((host.to_owned(), port));
119 }
120}
121
122#[cfg(any(test, feature = "fake"))]
123impl HostPorts for FakePorts {
124 fn in_use(&self, port: u16, protocol: Protocol) -> bool {
125 self.taken.lock().unwrap_or_else(std::sync::PoisonError::into_inner).contains(&(port, protocol))
126 }
127
128 fn pick_free(&self, _protocol: Protocol) -> Option<HeldPort> {
129 self.free
130 .lock()
131 .unwrap_or_else(std::sync::PoisonError::into_inner)
132 .pop_front()
133 .map(|p| HeldPort::new(p, ()))
134 }
135
136 fn reachable<'a>(&'a self, host: &'a str, port: u16, within: Duration) -> BoxFuture<'a, bool> {
137 let up = self
138 .answering
139 .lock()
140 .unwrap_or_else(std::sync::PoisonError::into_inner)
141 .contains(&(host.to_owned(), port));
142 Box::pin(async move {
143 if !up {
144 tokio::time::sleep(within).await;
145 }
146 up
147 })
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 fn port_free_over_both() -> u16 {
158 let start = 20_000 + u16::try_from(std::process::id() % 10_000).unwrap();
159 (start..32_768)
160 .chain(20_000..start)
161 .find(|&p| !LoopbackPorts.in_use(p, Protocol::Tcp) && !LoopbackPorts.in_use(p, Protocol::Udp))
162 .unwrap()
163 }
164
165 #[test]
166 fn a_held_port_is_in_use_over_its_protocol_only() {
167 let tcp_port = port_free_over_both();
168 let _tcp = TcpListener::bind(("127.0.0.1", tcp_port)).unwrap();
169 assert!(LoopbackPorts.in_use(tcp_port, Protocol::Tcp));
170 assert!(!LoopbackPorts.in_use(tcp_port, Protocol::Udp));
171 let udp_port = port_free_over_both();
172 let _udp = UdpSocket::bind(("127.0.0.1", udp_port)).unwrap();
173 assert!(LoopbackPorts.in_use(udp_port, Protocol::Udp), "a UDP holder (dnsmasq on 53) is caught");
174 assert!(!LoopbackPorts.in_use(udp_port, Protocol::Tcp));
175 }
176
177 #[test]
178 fn only_a_held_or_reserved_address_counts_on_the_ipv6_loopback() {
179 use std::io::{Error, ErrorKind};
180 assert!(held_elsewhere(&Error::from(ErrorKind::AddrInUse)));
181 assert!(held_elsewhere(&Error::from(ErrorKind::PermissionDenied)));
182 assert!(!held_elsewhere(&Error::from(ErrorKind::AddrNotAvailable)), "no IPv6 loopback here");
183 }
184
185 #[test]
186 fn picked_ports_are_held_until_dropped() {
187 let a = LoopbackPorts.pick_free(Protocol::Tcp).unwrap();
188 let b = LoopbackPorts.pick_free(Protocol::Tcp).unwrap();
189 assert_ne!(a.port, b.port);
190 assert!(LoopbackPorts.in_use(a.port, Protocol::Tcp));
191 }
192
193 #[tokio::test]
194 async fn a_listening_address_is_reachable() {
195 let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
196 let port = listener.local_addr().unwrap().port();
197 assert!(LoopbackPorts.reachable("127.0.0.1", port, Duration::from_secs(2)).await);
198 drop(listener);
199 assert!(!LoopbackPorts.reachable("127.0.0.1", port, Duration::from_secs(2)).await);
200 }
201}