agentmux_srv/backend/
lan_ports.rs1use std::net::Ipv4Addr;
25use std::ops::RangeInclusive;
26
27use tokio::net::TcpListener;
28
29pub const LAN_PORT_RANGE: RangeInclusive<u16> = 29700..=29799;
47
48pub struct StartupListeners {
50 pub web: TcpListener,
51 pub ws: TcpListener,
52 pub in_range: bool,
53}
54
55pub async fn bind_startup_listeners() -> StartupListeners {
59 bind_startup_listeners_from(LAN_PORT_RANGE).await
60}
61
62pub(crate) async fn bind_startup_listeners_from(
66 candidates: impl IntoIterator<Item = u16>,
67) -> StartupListeners {
68 if let Some((web, ws)) = first_two_free(candidates).await {
69 return StartupListeners {
70 web,
71 ws,
72 in_range: true,
73 };
74 }
75 let any = (Ipv4Addr::LOCALHOST, 0);
76 let web = TcpListener::bind(any)
77 .await
78 .unwrap_or_else(|e| panic!("failed to bind web listener on 127.0.0.1:0: {e}"));
79 let ws = TcpListener::bind(any)
80 .await
81 .unwrap_or_else(|e| panic!("failed to bind ws listener on 127.0.0.1:0: {e}"));
82 StartupListeners {
83 web,
84 ws,
85 in_range: false,
86 }
87}
88
89async fn first_two_free(
93 candidates: impl IntoIterator<Item = u16>,
94) -> Option<(TcpListener, TcpListener)> {
95 let mut bound = Vec::with_capacity(2);
96 for port in candidates {
97 if let Ok(listener) = TcpListener::bind((Ipv4Addr::LOCALHOST, port)).await {
99 bound.push(listener);
100 if bound.len() == 2 {
101 let ws = bound.pop()?;
102 let web = bound.pop()?;
103 return Some((web, ws));
104 }
105 }
106 }
107 None
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 static PORT_TESTS: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
120
121 fn free_port() -> u16 {
123 std::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0))
124 .unwrap()
125 .local_addr()
126 .unwrap()
127 .port()
128 }
129
130 fn two_free_ports() -> (u16, u16) {
132 let a = free_port();
133 loop {
134 let b = free_port();
135 if b != a {
136 return (a, b);
137 }
138 }
139 }
140
141 #[tokio::test]
142 async fn takes_the_first_two_free_ports_in_order() {
143 let _serial = PORT_TESTS.lock().await;
144 let (a, b) = two_free_ports();
145 let got = bind_startup_listeners_from([a, b]).await;
146 assert!(got.in_range);
147 assert_eq!(got.web.local_addr().unwrap().port(), a);
148 assert_eq!(got.ws.local_addr().unwrap().port(), b);
149 }
150
151 #[tokio::test]
152 async fn skips_a_port_another_instance_holds() {
153 let _serial = PORT_TESTS.lock().await;
154 let taken = std::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
156 let taken_port = taken.local_addr().unwrap().port();
157 let (b, c) = two_free_ports();
158 let got = bind_startup_listeners_from([taken_port, b, c]).await;
159 assert!(
160 got.in_range,
161 "two free ports remain, so it is still in range"
162 );
163 assert_eq!(got.web.local_addr().unwrap().port(), b);
164 assert_eq!(got.ws.local_addr().unwrap().port(), c);
165 drop(taken);
166 }
167
168 #[tokio::test]
169 async fn a_taken_port_between_two_free_ones_is_skipped_not_paired() {
170 let _serial = PORT_TESTS.lock().await;
171 let taken = std::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
174 let taken_port = taken.local_addr().unwrap().port();
175 let (a, c) = two_free_ports();
176 let got = bind_startup_listeners_from([a, taken_port, c]).await;
177 assert!(got.in_range);
178 assert_eq!(got.web.local_addr().unwrap().port(), a);
179 assert_eq!(
180 got.ws.local_addr().unwrap().port(),
181 c,
182 "not adjacent, and that is fine"
183 );
184 drop(taken);
185 }
186
187 #[tokio::test]
188 async fn fewer_than_two_free_falls_back_to_os_chosen_ports() {
189 let _serial = PORT_TESTS.lock().await;
190 let taken = std::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
191 let taken_port = taken.local_addr().unwrap().port();
192 let only_one_free = free_port();
193 let got = bind_startup_listeners_from([taken_port, only_one_free]).await;
194 assert!(!got.in_range, "one free port is not enough for the pair");
195 let (web, ws) = (got.web.local_addr().unwrap(), got.ws.local_addr().unwrap());
196 assert!(web.ip().is_loopback() && ws.ip().is_loopback());
197 assert_ne!(web.port(), ws.port());
198 assert_ne!(web.port(), taken_port);
199 assert!(std::net::TcpListener::bind((Ipv4Addr::LOCALHOST, only_one_free)).is_ok());
201 drop(taken);
202 }
203
204 #[tokio::test]
205 async fn an_empty_candidate_list_falls_back() {
206 let _serial = PORT_TESTS.lock().await;
207 let got = bind_startup_listeners_from(std::iter::empty()).await;
208 assert!(!got.in_range);
209 assert!(got.web.local_addr().unwrap().ip().is_loopback());
210 }
211
212 #[test]
213 fn the_range_is_one_a_rule_can_rely_on() {
214 let (start, end) = (*LAN_PORT_RANGE.start(), *LAN_PORT_RANGE.end());
215 assert!(start >= 1024, "not a well-known port");
216 assert!(
217 end < 32768,
218 "must stay below Linux's default ephemeral start (32768); Windows and macOS start at 49152"
219 );
220 for neighbour in [21027, 22000, 24800, 25565, 27015, 27017, 28015, 30000] {
221 assert!(
222 !LAN_PORT_RANGE.contains(&neighbour),
223 "{neighbour} is a well-known neighbour; a rule on the range would expose it"
224 );
225 }
226 assert!(
227 usize::from(end - start) + 1 >= 100,
228 "room for about fifty instances (two ports each)"
229 );
230 }
231}