[READ-ONLY] Mirror of https://github.com/mrgnw/dlna-rs. Single-binary DLNA media server in Rust. Auto-discovered by VLC on Apple TV.
0

Configure Feed

Select the types of activity you want to include in your feed.

vendor: harden SSDP IP selection for multi-homed hosts

Restrict NOTIFY/M-SEARCH and discover responses to real LAN interfaces
(skip bridge/vmenet/utun/awdl/llw), advertise ContentDirectory and
ConnectionManager service targets, and pick a routable source IP for
unicast discover replies. Fixes discovery on hosts with Tailscale and
phantom subnets where Location pointed at unreachable IPs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+324 -221
+2
vendor/librqbit-upnp-serve/src/constants.rs
··· 1 1 pub const UPNP_DEVICE_ROOT: &str = "upnp:rootdevice"; 2 2 pub const UPNP_DEVICE_MEDIASERVER: &str = "urn:schemas-upnp-org:device:MediaServer:1"; 3 + pub const UPNP_SERVICE_CONTENT_DIRECTORY: &str = "urn:schemas-upnp-org:service:ContentDirectory:1"; 4 + pub const UPNP_SERVICE_CONNECTION_MANAGER: &str = "urn:schemas-upnp-org:service:ConnectionManager:1"; 3 5 pub const SSDP_ALL: &str = "ssdp:all"; 4 6 5 7 pub const SOAP_ACTION_CONTENT_DIRECTORY_BROWSE: &[u8] =
+322 -221
vendor/librqbit-upnp-serve/src/ssdp.rs
··· 1 1 use std::{ 2 - net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}, 3 - time::Duration, 2 + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6, UdpSocket}, 3 + time::Duration, 4 4 }; 5 5 6 - use anyhow::{Context, bail}; 6 + use anyhow::{bail, Context}; 7 7 use bstr::BStr; 8 8 use librqbit_dualstack_sockets::{MulticastOpts, MulticastUdpSocket}; 9 9 use tokio_util::sync::CancellationToken; 10 10 use tracing::{debug, trace, warn}; 11 11 12 - use crate::constants::{SSDP_ALL, UPNP_DEVICE_MEDIASERVER, UPNP_DEVICE_ROOT}; 12 + use crate::constants::{ 13 + SSDP_ALL, UPNP_DEVICE_MEDIASERVER, UPNP_DEVICE_ROOT, UPNP_SERVICE_CONNECTION_MANAGER, 14 + UPNP_SERVICE_CONTENT_DIRECTORY, 15 + }; 13 16 14 17 const SSDP_PORT: u16 = 1900; 15 18 const SSDP_MCAST_IPV4: SocketAddrV4 = 16 - SocketAddrV4::new(Ipv4Addr::new(239, 255, 255, 250), SSDP_PORT); 19 + SocketAddrV4::new(Ipv4Addr::new(239, 255, 255, 250), SSDP_PORT); 17 20 #[allow(unused)] 18 21 const SSDP_MCAST_IPV6_LINK_LOCAL: SocketAddrV6 = SocketAddrV6::new( 19 - Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 0xc), 20 - SSDP_PORT, 21 - 0, 22 - 0, 22 + Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 0xc), 23 + SSDP_PORT, 24 + 0, 25 + 0, 23 26 ); 24 27 const SSDP_MCAST_IPV6_SITE_LOCAL: SocketAddrV6 = SocketAddrV6::new( 25 - Ipv6Addr::new(0xff05, 0, 0, 0, 0, 0, 0, 0xc), 26 - SSDP_PORT, 27 - 0, 28 - 0, 28 + Ipv6Addr::new(0xff05, 0, 0, 0, 0, 0, 0, 0xc), 29 + SSDP_PORT, 30 + 0, 31 + 0, 29 32 ); 30 33 31 34 const NTS_ALIVE: &str = "ssdp:alive"; ··· 33 36 34 37 #[derive(Debug)] 35 38 pub enum SsdpMessage<'a, 'h> { 36 - MSearch(SsdpMSearchRequest<'a>), 37 - #[allow(dead_code)] 38 - OtherRequest(httparse::Request<'h, 'a>), 39 - #[allow(dead_code)] 40 - Response(httparse::Response<'h, 'a>), 39 + MSearch(SsdpMSearchRequest<'a>), 40 + #[allow(dead_code)] 41 + OtherRequest(httparse::Request<'h, 'a>), 42 + #[allow(dead_code)] 43 + Response(httparse::Response<'h, 'a>), 41 44 } 42 45 43 46 #[derive(Debug)] 44 47 pub struct SsdpMSearchRequest<'a> { 45 - #[allow(dead_code)] 46 - pub host: &'a BStr, 47 - pub man: &'a BStr, 48 - pub st: &'a BStr, 48 + #[allow(dead_code)] 49 + pub host: &'a BStr, 50 + pub man: &'a BStr, 51 + pub st: &'a BStr, 49 52 } 50 53 51 54 impl SsdpMSearchRequest<'_> { 52 - fn matches_media_server(&self) -> bool { 53 - if self.man != "\"ssdp:discover\"" { 54 - return false; 55 - } 56 - if self.st == SSDP_ALL || self.st == UPNP_DEVICE_ROOT || self.st == UPNP_DEVICE_MEDIASERVER { 57 - return true; 58 - } 59 - false 60 - } 55 + fn matches_media_server(&self) -> bool { 56 + if self.man != "\"ssdp:discover\"" { 57 + return false; 58 + } 59 + if self.st == SSDP_ALL 60 + || self.st == UPNP_DEVICE_ROOT 61 + || self.st == UPNP_DEVICE_MEDIASERVER 62 + || self.st == UPNP_SERVICE_CONTENT_DIRECTORY 63 + || self.st == UPNP_SERVICE_CONNECTION_MANAGER 64 + { 65 + return true; 66 + } 67 + false 68 + } 61 69 } 62 70 63 71 pub fn try_parse_ssdp<'a, 'h>( 64 - buf: &'a [u8], 65 - headers: &'h mut [httparse::Header<'a>], 72 + buf: &'a [u8], 73 + headers: &'h mut [httparse::Header<'a>], 66 74 ) -> anyhow::Result<SsdpMessage<'a, 'h>> { 67 - if buf.starts_with(b"HTTP/") { 68 - let mut resp = httparse::Response::new(headers); 69 - resp.parse(buf).context("error parsing response")?; 70 - return Ok(SsdpMessage::Response(resp)); 71 - } 75 + if buf.starts_with(b"HTTP/") { 76 + let mut resp = httparse::Response::new(headers); 77 + resp.parse(buf).context("error parsing response")?; 78 + return Ok(SsdpMessage::Response(resp)); 79 + } 72 80 73 - let mut req = httparse::Request::new(headers); 74 - req.parse(buf).context("error parsing request")?; 81 + let mut req = httparse::Request::new(headers); 82 + req.parse(buf).context("error parsing request")?; 75 83 76 - match req.method { 77 - Some("M-SEARCH") => { 78 - let mut host = None; 79 - let mut man = None; 80 - let mut st = None; 84 + match req.method { 85 + Some("M-SEARCH") => { 86 + let mut host = None; 87 + let mut man = None; 88 + let mut st = None; 81 89 82 - for header in req.headers.iter() { 83 - match header.name { 84 - "HOST" | "Host" | "host" => host = Some(header.value), 85 - "MAN" | "Man" | "man" => man = Some(header.value), 86 - "ST" | "St" | "st" => st = Some(header.value), 87 - other => trace!(header=?BStr::new(other), "ignoring SSDP header"), 88 - } 89 - } 90 + for header in req.headers.iter() { 91 + match header.name { 92 + "HOST" | "Host" | "host" => host = Some(header.value), 93 + "MAN" | "Man" | "man" => man = Some(header.value), 94 + "ST" | "St" | "st" => st = Some(header.value), 95 + other => trace!(header=?BStr::new(other), "ignoring SSDP header"), 96 + } 97 + } 90 98 91 - match (host, man, st) { 92 - (Some(host), Some(man), Some(st)) => Ok(SsdpMessage::MSearch(SsdpMSearchRequest { 93 - host: BStr::new(host), 94 - man: BStr::new(man), 95 - st: BStr::new(st), 96 - })), 97 - _ => bail!("not all of host, man and st are set"), 98 - } 99 - } 100 - _ => Ok(SsdpMessage::OtherRequest(req)), 101 - } 99 + match (host, man, st) { 100 + (Some(host), Some(man), Some(st)) => Ok(SsdpMessage::MSearch(SsdpMSearchRequest { 101 + host: BStr::new(host), 102 + man: BStr::new(man), 103 + st: BStr::new(st), 104 + })), 105 + _ => bail!("not all of host, man and st are set"), 106 + } 107 + } 108 + _ => Ok(SsdpMessage::OtherRequest(req)), 109 + } 102 110 } 103 111 104 112 pub struct SsdpRunnerOptions { 105 - pub usn: String, 106 - pub description_http_location: url::Url, 107 - pub server_string: String, 108 - pub notify_interval: Duration, 109 - pub shutdown: CancellationToken, 113 + pub usn: String, 114 + pub description_http_location: url::Url, 115 + pub server_string: String, 116 + pub notify_interval: Duration, 117 + pub shutdown: CancellationToken, 110 118 } 111 119 112 120 pub struct SsdpRunner { 113 - opts: SsdpRunnerOptions, 114 - socket: MulticastUdpSocket, 121 + opts: SsdpRunnerOptions, 122 + socket: MulticastUdpSocket, 123 + } 124 + 125 + fn advertised_targets() -> Vec<&'static str> { 126 + vec![ 127 + UPNP_DEVICE_ROOT, 128 + UPNP_DEVICE_MEDIASERVER, 129 + UPNP_SERVICE_CONTENT_DIRECTORY, 130 + UPNP_SERVICE_CONNECTION_MANAGER, 131 + ] 115 132 } 116 133 117 134 impl SsdpRunner { 118 - pub async fn new(opts: SsdpRunnerOptions) -> anyhow::Result<Self> { 119 - let socket = MulticastUdpSocket::new( 120 - (Ipv6Addr::UNSPECIFIED, SSDP_PORT).into(), 121 - SSDP_MCAST_IPV4, 122 - SSDP_MCAST_IPV6_SITE_LOCAL, 123 - None, 124 - // Some(SSDP_MCAST_IPV6_LINK_LOCAL), 125 - None, 126 - ) 127 - .await 128 - .context("error creating SSDP socket")?; 135 + pub async fn new(opts: SsdpRunnerOptions) -> anyhow::Result<Self> { 136 + let socket = MulticastUdpSocket::new( 137 + (Ipv4Addr::UNSPECIFIED, SSDP_PORT).into(), 138 + SSDP_MCAST_IPV4, 139 + SSDP_MCAST_IPV6_SITE_LOCAL, 140 + None, 141 + // Some(SSDP_MCAST_IPV6_LINK_LOCAL), 142 + None, 143 + ) 144 + .await 145 + .context("error creating SSDP socket")?; 129 146 130 - Ok(Self { opts, socket }) 131 - } 147 + Ok(Self { opts, socket }) 148 + } 132 149 133 - fn generate_notify_message( 134 - &self, 135 - device_kind: &str, 136 - nts: &str, 137 - opts: &MulticastOpts, 138 - ) -> String { 139 - let usn: &str = &self.opts.usn; 140 - let server: &str = &self.opts.server_string; 141 - let host = addr_no_scope(&opts.mcast_addr()); 142 - let mut location = self.opts.description_http_location.clone(); 143 - let _ = location.set_ip_host(opts.iface_ip()); 144 - format!( 145 - "NOTIFY * HTTP/1.1\r 150 + fn generate_notify_message( 151 + &self, 152 + device_kind: &str, 153 + nts: &str, 154 + opts: &MulticastOpts, 155 + ) -> Option<String> { 156 + if !is_discoverable_local_iface_ip(opts.iface_ip(), self.socket.nics()) { 157 + trace!(?opts, "skipping SSDP notify on non-discoverable interface"); 158 + return None; 159 + } 160 + let usn: &str = &self.opts.usn; 161 + let server: &str = &self.opts.server_string; 162 + let host = addr_no_scope(&opts.mcast_addr()); 163 + let mut location = self.opts.description_http_location.clone(); 164 + let _ = location.set_ip_host(opts.iface_ip()); 165 + Some(format!( 166 + "NOTIFY * HTTP/1.1\r 146 167 Host: {host}\r 147 168 Cache-Control: max-age=75\r 148 169 Location: {location}\r ··· 152 173 USN: {usn}::{device_kind}\r 153 174 \r 154 175 " 155 - ) 156 - } 176 + )) 177 + } 157 178 158 - fn generate_ssdp_discover_response( 159 - &self, 160 - st: &str, 161 - addr: SocketAddr, 162 - ) -> anyhow::Result<Option<String>> { 163 - if matches!(addr.ip(), IpAddr::V6(a) if a.is_unicast_link_local()) { 164 - // VLC doesn't work with link-local URLs no matter what I tried. Furthermore, it probably 165 - // wants an interface name in its scope id, which we of course don't know as its local to 166 - // the client. 167 - debug!(?addr, "refusing to reply to a link-local address"); 168 - return Ok(None); 169 - } 170 - let local_ip = ::librqbit_upnp::get_local_ip_relative_to(addr, self.socket.nics())?; 171 - let location = { 172 - let mut loc = self.opts.description_http_location.clone(); 173 - let _ = loc.set_ip_host(local_ip); 174 - loc 175 - }; 176 - let usn = &self.opts.usn; 177 - let server = &self.opts.server_string; 178 - Ok(Some(format!( 179 - "HTTP/1.1 200 OK\r 179 + fn generate_ssdp_discover_response( 180 + &self, 181 + st: &str, 182 + addr: SocketAddr, 183 + ) -> anyhow::Result<Option<String>> { 184 + if matches!(addr.ip(), IpAddr::V6(a) if a.is_unicast_link_local()) { 185 + // VLC doesn't work with link-local URLs no matter what I tried. Furthermore, it probably 186 + // wants an interface name in its scope id, which we of course don't know as its local to 187 + // the client. 188 + debug!(?addr, "refusing to reply to a link-local address"); 189 + return Ok(None); 190 + } 191 + let local_ip = if addr.ip().is_loopback() { 192 + private_ipv4_addr(self.socket.nics()) 193 + .or_else(|| local_ip_for_unicast_reply(addr)) 194 + .map(Ok) 195 + .unwrap_or_else(|| { 196 + ::librqbit_upnp::get_local_ip_relative_to(addr, self.socket.nics()) 197 + })? 198 + } else { 199 + local_ip_for_unicast_reply(addr) 200 + .map(Ok) 201 + .unwrap_or_else(|| { 202 + ::librqbit_upnp::get_local_ip_relative_to(addr, self.socket.nics()) 203 + })? 204 + }; 205 + let location = { 206 + let mut loc = self.opts.description_http_location.clone(); 207 + let _ = loc.set_ip_host(local_ip); 208 + loc 209 + }; 210 + let usn = &self.opts.usn; 211 + let server = &self.opts.server_string; 212 + Ok(Some(format!( 213 + "HTTP/1.1 200 OK\r 180 214 Cache-Control: max-age=75\r 181 215 Ext: \r 182 216 Location: {location}\r ··· 184 218 St: {st}\r 185 219 Usn: {usn}::{st}\r 186 220 Content-Length: 0\r\n\r\n" 187 - ))) 188 - } 221 + ))) 222 + } 189 223 190 - async fn try_send_notifies(&self, nts: &str) { 191 - self.socket 192 - .try_send_mcast_everywhere(&|opts| { 193 - self.generate_notify_message(UPNP_DEVICE_MEDIASERVER, nts, opts) 194 - .into() 195 - }) 196 - .await 197 - } 224 + async fn try_send_notifies(&self, nts: &str) { 225 + for target in advertised_targets() { 226 + self.socket 227 + .try_send_mcast_everywhere(&|opts| { 228 + self.generate_notify_message(target, nts, opts) 229 + }) 230 + .await; 231 + } 232 + } 198 233 199 - async fn task_send_alive_notifies_periodically(&self) { 200 - let mut interval = tokio::time::interval(self.opts.notify_interval); 201 - loop { 202 - interval.tick().await; 203 - self.try_send_notifies(NTS_ALIVE).await; 204 - } 205 - } 234 + async fn task_send_alive_notifies_periodically(&self) { 235 + let mut interval = tokio::time::interval(self.opts.notify_interval); 236 + loop { 237 + interval.tick().await; 238 + self.try_send_notifies(NTS_ALIVE).await; 239 + } 240 + } 206 241 207 - async fn process_incoming_message(&self, msg: &[u8], addr: SocketAddr) -> anyhow::Result<()> { 208 - let mut headers = [httparse::EMPTY_HEADER; 16]; 209 - trace!(content = ?BStr::new(msg), ?addr, "received message"); 210 - let parsed = try_parse_ssdp(msg, &mut headers); 211 - let msg = match parsed { 212 - Ok(SsdpMessage::MSearch(msg)) => msg, 213 - Ok(m) => { 214 - trace!("ignoring {m:?}"); 215 - return Ok(()); 216 - } 217 - Err(e) => { 218 - debug!(error=?e, "error parsing SSDP message"); 219 - return Ok(()); 220 - } 221 - }; 222 - if !msg.matches_media_server() { 223 - trace!("not a media server request, ignoring"); 224 - return Ok(()); 225 - } 242 + async fn process_incoming_message(&self, msg: &[u8], addr: SocketAddr) -> anyhow::Result<()> { 243 + if is_excluded_local_iface_ip(addr.ip(), self.socket.nics()) { 244 + trace!( 245 + ?addr, 246 + "ignoring SSDP message from non-discoverable local interface" 247 + ); 248 + return Ok(()); 249 + } 250 + let mut headers = [httparse::EMPTY_HEADER; 16]; 251 + trace!(content = ?BStr::new(msg), ?addr, "received message"); 252 + let parsed = try_parse_ssdp(msg, &mut headers); 253 + let msg = match parsed { 254 + Ok(SsdpMessage::MSearch(msg)) => msg, 255 + Ok(m) => { 256 + trace!("ignoring {m:?}"); 257 + return Ok(()); 258 + } 259 + Err(e) => { 260 + debug!(error=?e, "error parsing SSDP message"); 261 + return Ok(()); 262 + } 263 + }; 264 + if !msg.matches_media_server() { 265 + trace!("not a media server request, ignoring"); 266 + return Ok(()); 267 + } 226 268 227 - if let Ok(st) = std::str::from_utf8(msg.st) { 228 - let response = self.generate_ssdp_discover_response(st, addr)?; 229 - if let Some(response) = response { 230 - trace!(content = response, ?addr, "sending SSDP discover response"); 231 - self.socket 232 - .send_to(response.as_bytes(), addr) 233 - .await 234 - .context("error sending")?; 235 - } 236 - } 269 + if let Ok(st) = std::str::from_utf8(msg.st) { 270 + let targets = if st == SSDP_ALL { 271 + advertised_targets() 272 + } else { 273 + vec![st] 274 + }; 275 + for target in targets { 276 + let response = self.generate_ssdp_discover_response(target, addr)?; 277 + if let Some(response) = response { 278 + trace!(content = response, ?addr, "sending SSDP discover response"); 279 + self.socket 280 + .send_to(response.as_bytes(), addr) 281 + .await 282 + .context("error sending")?; 283 + } 284 + } 285 + } 237 286 238 - Ok(()) 239 - } 287 + Ok(()) 288 + } 240 289 241 - async fn task_respond_on_msearches(&self) { 242 - let mut buf = vec![0u8; 16184]; 290 + async fn task_respond_on_msearches(&self) { 291 + let mut buf = vec![0u8; 16184]; 243 292 244 - loop { 245 - let (sz, addr) = match self.socket.recv_from(&mut buf).await { 246 - Ok((sz, addr)) => (sz, addr), 247 - Err(e) => { 248 - warn!(error=?e, "error receiving"); 249 - return; 250 - } 251 - }; 252 - let msg = &buf[..sz]; 253 - if let Err(e) = self.process_incoming_message(msg, addr).await { 254 - warn!(error=?e, ?addr, "error processing incoming SSDP message") 255 - } 256 - } 257 - } 293 + loop { 294 + let (sz, addr) = match self.socket.recv_from(&mut buf).await { 295 + Ok((sz, addr)) => (sz, addr), 296 + Err(e) => { 297 + warn!(error=?e, "error receiving"); 298 + return; 299 + } 300 + }; 301 + let msg = &buf[..sz]; 302 + if let Err(e) = self.process_incoming_message(msg, addr).await { 303 + warn!(error=?e, ?addr, "error processing incoming SSDP message") 304 + } 305 + } 306 + } 258 307 259 - async fn try_send_example_msearch(&self) { 260 - self.socket 261 - .try_send_mcast_everywhere(&|opts| { 262 - let dest = addr_no_scope(&opts.mcast_addr()); 263 - format!( 264 - "M-SEARCH * HTTP/1.1\r 308 + async fn try_send_example_msearch(&self) { 309 + self.socket 310 + .try_send_mcast_everywhere(&|opts| { 311 + if !is_discoverable_local_iface_ip(opts.iface_ip(), self.socket.nics()) { 312 + trace!( 313 + ?opts, 314 + "skipping SSDP m-search on non-discoverable interface" 315 + ); 316 + return None; 317 + } 318 + let dest = addr_no_scope(&opts.mcast_addr()); 319 + Some(format!( 320 + "M-SEARCH * HTTP/1.1\r 265 321 HOST: {dest}\r 266 322 ST: urn:schemas-upnp-org:device:MediaServer:1\r 267 323 MAN: \"ssdp:discover\"\r 268 324 MX: 2\r\n\r\n" 269 - ) 270 - .into() 271 - }) 272 - .await 273 - } 325 + )) 326 + }) 327 + .await 328 + } 274 329 275 - pub async fn run_forever(&self) -> anyhow::Result<()> { 276 - // This isn't necessary, but would show that it works. 277 - let t0 = self.try_send_example_msearch(); 278 - let t1 = self.task_respond_on_msearches(); 279 - let t2 = self.task_send_alive_notifies_periodically(); 330 + pub async fn run_forever(&self) -> anyhow::Result<()> { 331 + // This isn't necessary, but would show that it works. 332 + let t0 = self.try_send_example_msearch(); 333 + let t1 = self.task_respond_on_msearches(); 334 + let t2 = self.task_send_alive_notifies_periodically(); 280 335 281 - let wait = async move { 282 - tokio::join!(t0, t1, t2); 283 - Ok(()) 284 - }; 336 + let wait = async move { 337 + tokio::join!(t0, t1, t2); 338 + Ok(()) 339 + }; 285 340 286 - tokio::select! { 287 - r = wait => r, 288 - _ = self.opts.shutdown.cancelled() => { 289 - self.try_send_notifies(NTS_BYEBYE).await; 290 - Ok(()) 291 - } 292 - } 293 - } 341 + tokio::select! { 342 + r = wait => r, 343 + _ = self.opts.shutdown.cancelled() => { 344 + self.try_send_notifies(NTS_BYEBYE).await; 345 + Ok(()) 346 + } 347 + } 348 + } 294 349 } 295 350 296 351 fn addr_no_scope(addr: &SocketAddr) -> SocketAddr { 297 - match addr { 298 - SocketAddr::V4(a) => SocketAddr::V4(*a), 299 - SocketAddr::V6(a) => { 300 - let mut a = *a; 301 - a.set_scope_id(0); 302 - SocketAddr::V6(a) 303 - } 304 - } 352 + match addr { 353 + SocketAddr::V4(a) => SocketAddr::V4(*a), 354 + SocketAddr::V6(a) => { 355 + let mut a = *a; 356 + a.set_scope_id(0); 357 + SocketAddr::V6(a) 358 + } 359 + } 360 + } 361 + 362 + fn local_ip_for_unicast_reply(addr: SocketAddr) -> Option<IpAddr> { 363 + let bind_addr = match addr { 364 + SocketAddr::V4(_) => SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), 0), 365 + SocketAddr::V6(_) => SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), 0), 366 + }; 367 + let socket = UdpSocket::bind(bind_addr).ok()?; 368 + socket.connect(addr).ok()?; 369 + socket.local_addr().ok().map(|addr| addr.ip()) 370 + } 371 + 372 + fn private_ipv4_addr(nics: &[network_interface::NetworkInterface]) -> Option<IpAddr> { 373 + nics.iter() 374 + .filter(|nic| is_discoverable_nic(&nic.name)) 375 + .flat_map(|nic| nic.addr.iter()) 376 + .filter_map(|addr| match addr.ip() { 377 + IpAddr::V4(ip) if ip.is_private() && !ip.is_loopback() => Some(IpAddr::V4(ip)), 378 + _ => None, 379 + }) 380 + .next() 381 + } 382 + 383 + fn is_discoverable_local_iface_ip( 384 + ip: IpAddr, 385 + nics: &[network_interface::NetworkInterface], 386 + ) -> bool { 387 + nics.iter() 388 + .filter(|nic| is_discoverable_nic(&nic.name)) 389 + .flat_map(|nic| nic.addr.iter()) 390 + .any(|addr| addr.ip() == ip) 391 + } 392 + 393 + fn is_excluded_local_iface_ip(ip: IpAddr, nics: &[network_interface::NetworkInterface]) -> bool { 394 + nics.iter() 395 + .filter(|nic| !is_discoverable_nic(&nic.name)) 396 + .flat_map(|nic| nic.addr.iter()) 397 + .any(|addr| addr.ip() == ip) 398 + } 399 + 400 + fn is_discoverable_nic(name: &str) -> bool { 401 + !(name.starts_with("bridge") 402 + || name.starts_with("vmenet") 403 + || name.starts_with("utun") 404 + || name.starts_with("awdl") 405 + || name.starts_with("llw")) 305 406 }