pitchfork_cli/proxy/
lan_ip.rs1use std::net::{Ipv4Addr, SocketAddrV4};
13
14const PROBE_HOST: Ipv4Addr = Ipv4Addr::new(1, 1, 1, 1);
16const PROBE_PORT: u16 = 53;
17
18pub async fn detect_lan_ip() -> Option<Ipv4Addr> {
29 if let Some(ip) = probe_default_route().await {
31 if let Some(iface) = find_interface_for_ip(&ip) {
32 if !is_virtual_interface(&iface) {
33 return Some(ip);
34 }
35 } else {
36 if !ip.is_loopback() && !ip.is_link_local() {
40 return Some(ip);
41 }
42 }
43 }
44
45 fallback_interface_ip()
47}
48
49pub async fn detect_lan_ip_if_changed(last: Ipv4Addr) -> Option<Ipv4Addr> {
51 let current = detect_lan_ip().await?;
52 (current != last).then_some(current)
53}
54
55async fn probe_default_route() -> Option<Ipv4Addr> {
60 let sock = tokio::net::UdpSocket::bind("0.0.0.0:0").await.ok()?;
61 let dest = SocketAddrV4::new(PROBE_HOST, PROBE_PORT);
62 sock.connect(dest).await.ok()?;
65 let local = sock.local_addr().ok()?;
66 let ip = local.ip();
67 match ip {
68 std::net::IpAddr::V4(v4) if !v4.is_unspecified() && !v4.is_loopback() => Some(v4),
69 _ => None,
70 }
71}
72
73#[allow(dead_code)] struct InterfaceInfo {
76 name: String,
77 ip: Ipv4Addr,
78 is_loopback: bool,
79}
80
81#[cfg(unix)]
84fn find_interface_for_ip(target: &Ipv4Addr) -> Option<InterfaceInfo> {
85 let addrs = nix::ifaddrs::getifaddrs().ok()?;
86 for iface in addrs {
87 let flags = iface.flags;
88 let is_loopback = flags.contains(nix::net::if_::InterfaceFlags::IFF_LOOPBACK);
89 let ip = match iface
90 .address
91 .as_ref()
92 .and_then(|a| a.as_sockaddr_in().map(|sa| sa.ip()))
93 {
94 Some(ip) => ip,
95 None => continue,
96 };
97 if &ip == target {
98 return Some(InterfaceInfo {
99 name: iface.interface_name.clone(),
100 ip,
101 is_loopback,
102 });
103 }
104 }
105 None
106}
107
108#[cfg(unix)]
109fn is_virtual_interface(iface: &InterfaceInfo) -> bool {
110 if iface.is_loopback {
111 return true;
112 }
113 if iface.ip.is_link_local() {
114 return true;
115 }
116 let name = iface.name.to_lowercase();
117 if name.starts_with("veth") || name.starts_with("br-") || name.starts_with("docker") {
119 return true;
120 }
121 if name.starts_with("virbr") {
123 return true;
124 }
125 if name.starts_with("vethernet") || name.starts_with("bridge") {
127 return true;
128 }
129 false
130}
131
132#[cfg(unix)]
133fn fallback_interface_ip() -> Option<Ipv4Addr> {
134 let addrs = nix::ifaddrs::getifaddrs().ok()?;
135 for iface in addrs {
136 let flags = iface.flags;
137 if !flags.contains(nix::net::if_::InterfaceFlags::IFF_UP) {
138 continue;
139 }
140 if flags.contains(nix::net::if_::InterfaceFlags::IFF_LOOPBACK) {
141 continue;
142 }
143 let ip = match iface
144 .address
145 .as_ref()
146 .and_then(|a| a.as_sockaddr_in().map(|sa| sa.ip()))
147 {
148 Some(ip) => ip,
149 None => continue,
150 };
151 if ip.is_loopback() || ip.is_link_local() {
152 continue;
153 }
154 let info = InterfaceInfo {
155 name: iface.interface_name.clone(),
156 ip,
157 is_loopback: false,
158 };
159 if !is_virtual_interface(&info) {
160 return Some(ip);
161 }
162 }
163 None
164}
165
166#[cfg(not(unix))]
169fn find_interface_for_ip(_target: &Ipv4Addr) -> Option<InterfaceInfo> {
170 None
171}
172
173#[cfg(not(unix))]
174fn is_virtual_interface(_iface: &InterfaceInfo) -> bool {
175 false
176}
177
178#[cfg(not(unix))]
179fn fallback_interface_ip() -> Option<Ipv4Addr> {
180 None
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[cfg(unix)]
188 #[test]
189 fn test_is_virtual_interface_loopback() {
190 let iface = InterfaceInfo {
191 name: "lo".to_string(),
192 ip: Ipv4Addr::new(127, 0, 0, 1),
193 is_loopback: true,
194 };
195 assert!(is_virtual_interface(&iface));
196 }
197
198 #[cfg(unix)]
199 #[test]
200 fn test_is_virtual_interface_docker() {
201 for name in &["veth1234", "br-abc", "docker0"] {
202 let iface = InterfaceInfo {
203 name: name.to_string(),
204 ip: Ipv4Addr::new(172, 17, 0, 1),
205 is_loopback: false,
206 };
207 assert!(
208 is_virtual_interface(&iface),
209 "expected {name} to be virtual"
210 );
211 }
212 }
213
214 #[cfg(unix)]
215 #[test]
216 fn test_is_virtual_interface_normal() {
217 let iface = InterfaceInfo {
218 name: "eth0".to_string(),
219 ip: Ipv4Addr::new(192, 168, 1, 42),
220 is_loopback: false,
221 };
222 assert!(!is_virtual_interface(&iface));
223 }
224
225 #[cfg(unix)]
226 #[test]
227 fn test_is_virtual_interface_link_local() {
228 let iface = InterfaceInfo {
229 name: "en0".to_string(),
230 ip: Ipv4Addr::new(169, 254, 1, 1),
231 is_loopback: false,
232 };
233 assert!(is_virtual_interface(&iface));
234 }
235}