1use std::net::SocketAddr;
14use thiserror::Error;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum PortBinding {
19 OsAssigned,
31
32 Explicit(u16),
41
42 Range(u16, u16),
51}
52
53impl Default for PortBinding {
54 fn default() -> Self {
55 Self::OsAssigned
56 }
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
61pub enum IpMode {
62 IPv4Only,
69
70 IPv6Only,
72
73 DualStack,
78
79 DualStackSeparate {
83 ipv4_port: PortBinding,
85 ipv6_port: PortBinding,
87 },
88}
89
90impl Default for IpMode {
91 fn default() -> Self {
92 Self::IPv4Only
93 }
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Default)]
98pub struct SocketOptions {
99 pub send_buffer_size: Option<usize>,
101 pub recv_buffer_size: Option<usize>,
103 pub reuse_address: bool,
105 pub reuse_port: bool,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum PortRetryBehavior {
112 FailFast,
114 FallbackToOsAssigned,
116 TryNext,
118}
119
120impl Default for PortRetryBehavior {
121 fn default() -> Self {
122 Self::FailFast
123 }
124}
125
126#[derive(Debug, Clone)]
163pub struct EndpointPortConfig {
164 pub port: PortBinding,
166 pub ip_mode: IpMode,
168 pub socket_options: SocketOptions,
170 pub retry_behavior: PortRetryBehavior,
172}
173
174impl Default for EndpointPortConfig {
175 fn default() -> Self {
176 Self {
177 port: PortBinding::OsAssigned,
179 ip_mode: IpMode::IPv4Only,
181 socket_options: SocketOptions::default(),
182 retry_behavior: PortRetryBehavior::FailFast,
183 }
184 }
185}
186
187#[derive(Debug, Error, Clone, PartialEq, Eq)]
189pub enum EndpointConfigError {
190 #[error("Port {0} is already in use. Try using PortBinding::OsAssigned to let the OS choose.")]
192 PortInUse(u16),
193
194 #[error("Invalid port number: {0}. Port must be in range 0-65535.")]
196 InvalidPort(u32),
197
198 #[error(
200 "Cannot bind to privileged port {0}. Use port 1024 or higher, or run with appropriate permissions."
201 )]
202 PermissionDenied(u16),
203
204 #[error(
206 "No available port in range {0}-{1}. Try a wider range or use PortBinding::OsAssigned."
207 )]
208 NoPortInRange(u16, u16),
209
210 #[error("Dual-stack not supported on this platform. Use IpMode::IPv4Only or IpMode::IPv6Only.")]
212 DualStackNotSupported,
213
214 #[error("IPv6 not available on this system. Use IpMode::IPv4Only.")]
216 Ipv6NotAvailable,
217
218 #[error("Failed to bind socket: {0}")]
220 BindFailed(String),
221
222 #[error("Invalid configuration: {0}")]
224 InvalidConfig(String),
225
226 #[error("IO error: {0}")]
228 IoError(String),
229}
230
231impl From<std::io::Error> for EndpointConfigError {
232 fn from(err: std::io::Error) -> Self {
233 use std::io::ErrorKind;
234 match err.kind() {
235 ErrorKind::AddrInUse => {
236 Self::BindFailed(err.to_string())
238 }
239 ErrorKind::PermissionDenied => Self::BindFailed(err.to_string()),
240 ErrorKind::AddrNotAvailable => Self::Ipv6NotAvailable,
241 _ => Self::IoError(err.to_string()),
242 }
243 }
244}
245
246pub type PortConfigResult<T> = Result<T, EndpointConfigError>;
248
249#[derive(Debug, Clone)]
251pub struct BoundSocket {
252 pub addrs: Vec<SocketAddr>,
254 pub config: EndpointPortConfig,
256}
257
258impl BoundSocket {
259 pub fn primary_addr(&self) -> Option<SocketAddr> {
261 self.addrs.first().copied()
262 }
263
264 pub fn all_addrs(&self) -> &[SocketAddr] {
266 &self.addrs
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn test_port_binding_default() {
276 let port = PortBinding::default();
277 assert_eq!(port, PortBinding::OsAssigned);
278 }
279
280 #[test]
281 fn test_port_binding_explicit() {
282 let port = PortBinding::Explicit(9000);
283 match port {
284 PortBinding::Explicit(9000) => {}
285 _ => panic!("Expected Explicit(9000)"),
286 }
287 }
288
289 #[test]
290 fn test_port_binding_range() {
291 let port = PortBinding::Range(9000, 9010);
292 match port {
293 PortBinding::Range(9000, 9010) => {}
294 _ => panic!("Expected Range(9000, 9010)"),
295 }
296 }
297
298 #[test]
299 fn test_ip_mode_default() {
300 let mode = IpMode::default();
301 assert_eq!(mode, IpMode::IPv4Only);
302 }
303
304 #[test]
305 fn test_ip_mode_ipv4_only() {
306 let mode = IpMode::IPv4Only;
307 match mode {
308 IpMode::IPv4Only => {}
309 _ => panic!("Expected IPv4Only"),
310 }
311 }
312
313 #[test]
314 fn test_ip_mode_dual_stack_separate() {
315 let mode = IpMode::DualStackSeparate {
316 ipv4_port: PortBinding::Explicit(9000),
317 ipv6_port: PortBinding::Explicit(9001),
318 };
319 match mode {
320 IpMode::DualStackSeparate {
321 ipv4_port,
322 ipv6_port,
323 } => {
324 assert_eq!(ipv4_port, PortBinding::Explicit(9000));
325 assert_eq!(ipv6_port, PortBinding::Explicit(9001));
326 }
327 _ => panic!("Expected DualStackSeparate"),
328 }
329 }
330
331 #[test]
332 fn test_socket_options_default() {
333 let opts = SocketOptions::default();
334 assert_eq!(opts.send_buffer_size, None);
335 assert_eq!(opts.recv_buffer_size, None);
336 assert!(!opts.reuse_address);
337 assert!(!opts.reuse_port);
338 }
339
340 #[test]
341 fn test_retry_behavior_default() {
342 let behavior = PortRetryBehavior::default();
343 assert_eq!(behavior, PortRetryBehavior::FailFast);
344 }
345
346 #[test]
347 fn test_endpoint_port_config_default() {
348 let config = EndpointPortConfig::default();
349 assert_eq!(config.port, PortBinding::OsAssigned);
350 assert_eq!(config.ip_mode, IpMode::IPv4Only);
351 assert_eq!(config.retry_behavior, PortRetryBehavior::FailFast);
352 }
353
354 #[test]
355 fn test_endpoint_config_error_display() {
356 let err = EndpointConfigError::PortInUse(9000);
357 assert!(err.to_string().contains("Port 9000 is already in use"));
358
359 let err = EndpointConfigError::InvalidPort(70000);
360 assert!(err.to_string().contains("Invalid port number"));
361
362 let err = EndpointConfigError::PermissionDenied(80);
363 assert!(err.to_string().contains("privileged port"));
364 }
365
366 #[test]
367 fn test_bound_socket() {
368 let config = EndpointPortConfig::default();
369 let addrs = vec![
370 "127.0.0.1:9000".parse().expect("valid address"),
371 "127.0.0.1:9001".parse().expect("valid address"),
372 ];
373 let bound = BoundSocket {
374 addrs: addrs.clone(),
375 config,
376 };
377
378 assert_eq!(bound.primary_addr(), Some(addrs[0]));
379 assert_eq!(bound.all_addrs(), &addrs[..]);
380 }
381}