use anyhow::Context; use rust_p2p_core::socket::LocalInterface; use socket2::{Domain, Protocol, Socket, Type}; use std::io; use std::net::SocketAddr; #[derive(Clone, Debug)] pub(crate) struct ResolvedInterface { pub(crate) name: String, pub(crate) socket_interface: LocalInterface, } /// 将配置中的网卡名称解析为底层 Socket 所需的接口标识。 /// Linux/Android 使用名称绑定,Windows/macOS 使用接口索引。 pub(crate) fn resolve_interface(name: Option<&str>) -> anyhow::Result> { let Some(name) = name.map(str::trim).filter(|name| !name.is_empty()) else { return Ok(None); }; let (canonical_name, index) = match getifaddrs::if_nametoindex(name) { Ok(index) => ( getifaddrs::if_indextoname(index).unwrap_or_else(|_| name.to_owned()), index, ), Err(_) => { let interface = getifaddrs::getifaddrs() .context("读取本机网卡列表失败")? .find(|interface| { if interface.name.eq_ignore_ascii_case(name) { return true; } #[cfg(windows)] { interface.description.eq_ignore_ascii_case(name) } #[cfg(not(windows))] { false } }) .ok_or_else(|| anyhow::anyhow!("找不到出口网卡 '{name}'"))?; let index = interface .index .ok_or_else(|| anyhow::anyhow!("出口网卡 '{}' 没有可用索引", interface.name))?; (interface.name, index) } }; #[cfg(any(target_os = "linux", target_os = "android"))] let interface = LocalInterface::new(canonical_name.clone()); #[cfg(not(any(target_os = "linux", target_os = "android")))] let interface = LocalInterface::new(index); // Linux/Android 只用名称,但仍执行 if_nametoindex 来提前校验配置。 let _ = index; Ok(Some(ResolvedInterface { name: canonical_name, socket_interface: interface, })) } pub(crate) trait SocketTrait { fn set_ip_unicast_if(&self, _interface: &LocalInterface) -> io::Result<()> { Ok(()) } } #[cfg(target_os = "windows")] impl SocketTrait for Socket { fn set_ip_unicast_if(&self, interface: &LocalInterface) -> io::Result<()> { use std::os::windows::io::AsRawSocket; use windows_sys::Win32::Networking::WinSock::{ IP_UNICAST_IF, IPPROTO_IP, SOCKET_ERROR, htonl, setsockopt, }; let raw_socket = self.as_raw_socket(); let result = unsafe { let best_interface = htonl(interface.index); setsockopt( raw_socket as usize, IPPROTO_IP, IP_UNICAST_IF, &best_interface as *const _ as *const u8, std::mem::size_of_val(&best_interface) as i32, ) }; if result == SOCKET_ERROR { return Err(io::Error::last_os_error()); } Ok(()) } } #[cfg(any(target_os = "linux", target_os = "android"))] impl SocketTrait for Socket { fn set_ip_unicast_if(&self, interface: &LocalInterface) -> io::Result<()> { self.bind_device(Some(interface.name.as_bytes())) } } #[cfg(any(target_os = "macos", target_os = "ios"))] impl SocketTrait for Socket { fn set_ip_unicast_if(&self, interface: &LocalInterface) -> io::Result<()> { self.bind_device_by_index_v4(std::num::NonZeroU32::new(interface.index)) } } #[cfg(target_os = "freebsd")] impl SocketTrait for Socket {} pub(crate) fn bind_socket_to_interface( socket: &Socket, interface: Option<&LocalInterface>, is_ipv4: bool, ) -> io::Result<()> { if is_ipv4 && let Some(interface) = interface { socket.set_ip_unicast_if(interface)?; } Ok(()) } pub(crate) fn bind_udp( addr: SocketAddr, interface: Option<&LocalInterface>, ) -> io::Result { let socket = rust_p2p_core::socket::bind_udp(addr, interface)?; tokio::net::UdpSocket::from_std(socket.into()) } pub(crate) async fn connect_tcp( addr: SocketAddr, interface: Option<&LocalInterface>, ) -> io::Result { let socket = Socket::new(Domain::for_address(addr), Type::STREAM, Some(Protocol::TCP))?; bind_socket_to_interface( &socket, interface, addr.is_ipv4() && !addr.ip().is_loopback(), )?; socket.set_nonblocking(true)?; socket.set_tcp_nodelay(true)?; match socket.connect(&addr.into()) { Ok(()) => {} Err(ref error) if error.kind() == io::ErrorKind::WouldBlock => {} #[cfg(unix)] Err(ref error) if error.raw_os_error() == Some(libc::EINPROGRESS) => {} Err(error) => return Err(error), } let stream = tokio::net::TcpStream::from_std(socket.into())?; stream.writable().await?; if let Some(error) = stream.take_error()? { return Err(error); } Ok(stream) } pub(crate) async fn connect_tcp_resolved( addr: A, interface: Option<&LocalInterface>, ) -> io::Result { let addrs = tokio::net::lookup_host(addr).await?.collect::>(); let mut last_error = None; for addr in addrs { match connect_tcp(addr, interface).await { Ok(stream) => return Ok(stream), Err(error) => last_error = Some(error), } } Err(last_error.unwrap_or_else(|| io::Error::other("目标地址解析结果为空"))) } #[cfg(test)] mod tests { use super::*; #[test] fn empty_interface_name_disables_binding() { assert!(resolve_interface(None).unwrap().is_none()); assert!(resolve_interface(Some(" ")).unwrap().is_none()); } #[test] fn invalid_interface_name_is_rejected() { let name = "vnt-interface-that-must-not-exist"; assert!(resolve_interface(Some(name)).is_err()); } #[test] fn existing_interface_name_resolves() { let interface = getifaddrs::getifaddrs() .unwrap() .find(|interface| interface.index.is_some()) .expect("at least one indexed interface"); let resolved = resolve_interface(Some(&interface.name)).unwrap().unwrap(); assert_eq!(resolved.name, interface.name); #[cfg(not(any(target_os = "linux", target_os = "android")))] assert_eq!(resolved.socket_interface.index, interface.index.unwrap()); } #[cfg(windows)] #[test] fn windows_friendly_name_resolves() { let interface = getifaddrs::getifaddrs() .unwrap() .find(|interface| interface.index.is_some() && !interface.description.is_empty()) .expect("at least one described interface"); let resolved = resolve_interface(Some(&interface.description)) .unwrap() .unwrap(); assert_eq!(resolved.socket_interface.index, interface.index.unwrap()); } }