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
73struct InterfaceInfo {
75 name: String,
76 ip: Ipv4Addr,
77 is_loopback: bool,
78}
79
80#[cfg(unix)]
83fn find_interface_for_ip(target: &Ipv4Addr) -> Option<InterfaceInfo> {
84 let addrs = nix::ifaddrs::getifaddrs().ok()?;
85 for iface in addrs {
86 let flags = iface.flags;
87 let is_loopback = flags.contains(nix::net::if_::InterfaceFlags::IFF_LOOPBACK);
88 let ip = match iface
89 .address
90 .as_ref()
91 .and_then(|a| a.as_sockaddr_in().map(|sa| sa.ip()))
92 {
93 Some(ip) => ip,
94 None => continue,
95 };
96 if &ip == target {
97 return Some(InterfaceInfo {
98 name: iface.interface_name.clone(),
99 ip,
100 is_loopback,
101 });
102 }
103 }
104 None
105}
106
107#[cfg(unix)]
108fn is_virtual_interface(iface: &InterfaceInfo) -> bool {
109 if iface.is_loopback {
110 return true;
111 }
112 if iface.ip.is_link_local() {
113 return true;
114 }
115 let name = iface.name.to_lowercase();
116 if name.starts_with("veth") || name.starts_with("br-") || name.starts_with("docker") {
118 return true;
119 }
120 if name.starts_with("virbr") {
122 return true;
123 }
124 if name.starts_with("vethernet") || name.starts_with("bridge") {
126 return true;
127 }
128 false
129}
130
131#[cfg(unix)]
132fn fallback_interface_ip() -> Option<Ipv4Addr> {
133 let addrs = nix::ifaddrs::getifaddrs().ok()?;
134 for iface in addrs {
135 let flags = iface.flags;
136 if !flags.contains(nix::net::if_::InterfaceFlags::IFF_UP) {
137 continue;
138 }
139 if flags.contains(nix::net::if_::InterfaceFlags::IFF_LOOPBACK) {
140 continue;
141 }
142 let ip = match iface
143 .address
144 .as_ref()
145 .and_then(|a| a.as_sockaddr_in().map(|sa| sa.ip()))
146 {
147 Some(ip) => ip,
148 None => continue,
149 };
150 if ip.is_loopback() || ip.is_link_local() {
151 continue;
152 }
153 let info = InterfaceInfo {
154 name: iface.interface_name.clone(),
155 ip,
156 is_loopback: false,
157 };
158 if !is_virtual_interface(&info) {
159 return Some(ip);
160 }
161 }
162 None
163}
164
165#[cfg(not(unix))]
168fn find_interface_for_ip(_target: &Ipv4Addr) -> Option<InterfaceInfo> {
169 None
170}
171
172#[cfg(not(unix))]
173fn is_virtual_interface(_iface: &InterfaceInfo) -> bool {
174 false
175}
176
177#[cfg(not(unix))]
178fn fallback_interface_ip() -> Option<Ipv4Addr> {
179 None
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[cfg(unix)]
187 #[test]
188 fn test_is_virtual_interface_loopback() {
189 let iface = InterfaceInfo {
190 name: "lo".to_string(),
191 ip: Ipv4Addr::new(127, 0, 0, 1),
192 is_loopback: true,
193 };
194 assert!(is_virtual_interface(&iface));
195 }
196
197 #[cfg(unix)]
198 #[test]
199 fn test_is_virtual_interface_docker() {
200 for name in &["veth1234", "br-abc", "docker0"] {
201 let iface = InterfaceInfo {
202 name: name.to_string(),
203 ip: Ipv4Addr::new(172, 17, 0, 1),
204 is_loopback: false,
205 };
206 assert!(
207 is_virtual_interface(&iface),
208 "expected {name} to be virtual"
209 );
210 }
211 }
212
213 #[cfg(unix)]
214 #[test]
215 fn test_is_virtual_interface_normal() {
216 let iface = InterfaceInfo {
217 name: "eth0".to_string(),
218 ip: Ipv4Addr::new(192, 168, 1, 42),
219 is_loopback: false,
220 };
221 assert!(!is_virtual_interface(&iface));
222 }
223
224 #[cfg(unix)]
225 #[test]
226 fn test_is_virtual_interface_link_local() {
227 let iface = InterfaceInfo {
228 name: "en0".to_string(),
229 ip: Ipv4Addr::new(169, 254, 1, 1),
230 is_loopback: false,
231 };
232 assert!(is_virtual_interface(&iface));
233 }
234}