1use local_ip_address::list_afinet_netifas;
2use serde::{Deserialize, Serialize};
3use std::collections::HashSet;
4use std::net::IpAddr;
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum BindHostKind {
9 Localhost,
10 AllInterfaces,
11 Interface,
12}
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub struct BindHostOption {
16 pub address: String,
17 pub kind: BindHostKind,
18 #[serde(default, skip_serializing_if = "Option::is_none")]
19 pub interface: Option<String>,
20}
21
22impl BindHostOption {
23 fn localhost() -> Self {
24 Self {
25 address: "127.0.0.1".to_string(),
26 kind: BindHostKind::Localhost,
27 interface: None,
28 }
29 }
30
31 fn all_interfaces() -> Self {
32 Self {
33 address: "0.0.0.0".to_string(),
34 kind: BindHostKind::AllInterfaces,
35 interface: None,
36 }
37 }
38}
39
40fn is_bindable_interface_ip(ip: &IpAddr) -> bool {
41 match ip {
42 IpAddr::V4(ipv4) => !ipv4.is_loopback() && !ipv4.is_link_local() && !ipv4.is_unspecified(),
43 IpAddr::V6(_) => false,
47 }
48}
49
50fn collect_bind_hosts_from_iter<I>(ifaces: I) -> Vec<BindHostOption>
51where
52 I: IntoIterator<Item = (String, IpAddr)>,
53{
54 let mut hosts = vec![
55 BindHostOption::localhost(),
56 BindHostOption::all_interfaces(),
57 ];
58 let mut seen: HashSet<String> = hosts.iter().map(|host| host.address.clone()).collect();
59
60 for (interface, ip) in ifaces {
61 if !is_bindable_interface_ip(&ip) {
62 continue;
63 }
64 let address = ip.to_string();
65 if !seen.insert(address.clone()) {
66 continue;
67 }
68 hosts.push(BindHostOption {
69 address,
70 kind: BindHostKind::Interface,
71 interface: Some(interface),
72 });
73 }
74
75 hosts
76}
77
78pub fn available_bind_hosts() -> Vec<BindHostOption> {
79 match list_afinet_netifas() {
80 Ok(ifaces) => collect_bind_hosts_from_iter(ifaces),
81 Err(_) => collect_bind_hosts_from_iter(std::iter::empty()),
82 }
83}
84
85pub fn host_in_list(host: &str, hosts: &[BindHostOption]) -> bool {
89 let h = host.trim();
90 if matches!(
91 h,
92 "" | "0.0.0.0" | "::" | "[::]" | "127.0.0.1" | "::1" | "[::1]"
93 ) {
94 return true;
95 }
96 let bare = h.trim_start_matches('[').trim_end_matches(']');
97 hosts
98 .iter()
99 .any(|opt| opt.address == h || opt.address == bare)
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
106
107 #[test]
108 fn bind_hosts_always_start_with_local_and_all_interfaces() {
109 let hosts = collect_bind_hosts_from_iter(std::iter::empty());
110 assert_eq!(
111 hosts,
112 vec![
113 BindHostOption::localhost(),
114 BindHostOption::all_interfaces()
115 ]
116 );
117 }
118
119 #[test]
120 fn host_in_list_passes_wildcard_and_loopback() {
121 let hosts = collect_bind_hosts_from_iter(std::iter::empty());
122 assert!(host_in_list("0.0.0.0", &hosts));
123 assert!(host_in_list("::", &hosts));
124 assert!(host_in_list("127.0.0.1", &hosts));
125 assert!(host_in_list("::1", &hosts));
126 assert!(!host_in_list("192.168.1.20", &hosts));
127 }
128
129 #[test]
130 fn host_in_list_matches_specific_interface_address() {
131 let hosts = collect_bind_hosts_from_iter([(
132 "en0".to_string(),
133 IpAddr::V4(Ipv4Addr::new(192, 168, 1, 20)),
134 )]);
135 assert!(host_in_list("192.168.1.20", &hosts));
136 assert!(!host_in_list("192.168.99.99", &hosts));
137 }
138
139 #[test]
140 fn bind_hosts_filter_non_bindable_ipv4_and_dedupe_results() {
141 let hosts = collect_bind_hosts_from_iter([
142 ("lo0".to_string(), IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
143 (
144 "en0".to_string(),
145 IpAddr::V4(Ipv4Addr::new(192, 168, 1, 20)),
146 ),
147 (
148 "en1".to_string(),
149 IpAddr::V4(Ipv4Addr::new(192, 168, 1, 20)),
150 ),
151 (
152 "en2".to_string(),
153 IpAddr::V4(Ipv4Addr::new(169, 254, 10, 20)),
154 ),
155 ("awdl0".to_string(), IpAddr::V6(Ipv6Addr::LOCALHOST)),
156 ("bridge0".to_string(), IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))),
157 ]);
158
159 assert_eq!(
160 hosts,
161 vec![
162 BindHostOption::localhost(),
163 BindHostOption::all_interfaces(),
164 BindHostOption {
165 address: "192.168.1.20".to_string(),
166 kind: BindHostKind::Interface,
167 interface: Some("en0".to_string()),
168 },
169 ]
170 );
171 }
172}