use std::io;
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, UdpSocket};
use std::time::Duration;
use socket2::{Domain, InterfaceIndexOrAddress, Protocol, SockAddr, Socket, Type};
use super::netif;
const READ_TIMEOUT: Duration = Duration::from_millis(500);
const MULTICAST_TTL: u32 = 3;
#[derive(Clone, Debug)]
enum Egress {
Address(Ipv4Addr),
Interface { name: String, index: u32 },
}
#[derive(Debug)]
pub(crate) struct Conn {
socket: UdpSocket,
target: SocketAddrV4,
}
impl Conn {
pub(crate) fn open(
target: Ipv4Addr,
port: u16,
local_ip: Option<Ipv4Addr>,
interface: Option<&str>,
) -> io::Result<Self> {
let multicast = target.is_multicast();
let egress = match interface {
Some(name) => match netif::address_of(name) {
Some(ip) => Egress::Address(ip),
None => Egress::Interface {
name: name.to_owned(),
index: netif::index_of(name)?,
},
},
None => Egress::Address(match local_ip {
Some(ip) => ip,
None => netif::default_local_ip()?,
}),
};
let socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))?;
socket.set_reuse_address(true)?;
socket.bind(&SockAddr::from(SocketAddrV4::new(
Ipv4Addr::UNSPECIFIED,
port,
)))?;
socket.set_read_timeout(Some(READ_TIMEOUT))?;
if multicast {
socket.set_multicast_ttl_v4(MULTICAST_TTL)?;
match &egress {
Egress::Address(ip) => {
socket.set_multicast_if_v4(ip)?;
socket.join_multicast_v4(&target, ip)?;
}
Egress::Interface { name, index } => {
socket.join_multicast_v4_n(&target, &InterfaceIndexOrAddress::Index(*index))?;
bind_to_interface(&socket, name)?;
}
}
} else {
socket.set_broadcast(true)?;
if let Egress::Interface { name, .. } = &egress {
bind_to_interface(&socket, name)?;
}
}
Ok(Self {
socket: socket.into(),
target: SocketAddrV4::new(target, port),
})
}
pub(super) fn send(&self, data: &[u8]) -> io::Result<usize> {
self.socket.send_to(data, self.target)
}
pub(crate) fn recv<'a>(&self, buf: &'a mut [u8]) -> io::Result<Option<(&'a [u8], Ipv4Addr)>> {
match self.socket.recv_from(buf) {
Ok((n, SocketAddr::V4(from))) => Ok(buf.get(..n).map(|data| (data, *from.ip()))),
Ok((_, SocketAddr::V6(_))) => Ok(None),
Err(e) if is_timeout(&e) => Ok(None),
Err(e) => Err(e),
}
}
pub(crate) fn target(&self) -> SocketAddrV4 {
self.target
}
#[cfg(test)]
pub(crate) fn reuse_port(&self) -> io::Result<bool> {
socket2::SockRef::from(&self.socket).reuse_port()
}
#[cfg(test)]
pub(crate) fn recv_within(&self, timeout: Duration) -> Option<Vec<u8>> {
self.socket.set_read_timeout(Some(timeout)).ok()?;
let mut buf = vec![0u8; 2048];
let got = self
.recv(&mut buf)
.ok()
.flatten()
.map(|(data, _)| data.to_vec());
let _ = self.socket.set_read_timeout(Some(READ_TIMEOUT));
got
}
}
#[cfg(any(target_os = "android", target_os = "linux"))]
fn bind_to_interface(socket: &Socket, name: &str) -> io::Result<()> {
socket.bind_device(Some(name.as_bytes()))
}
#[cfg(not(any(target_os = "android", target_os = "linux")))]
fn bind_to_interface(_socket: &Socket, name: &str) -> io::Result<()> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
format!("interface {name:?} has no IPv4 address, and only Linux can select an interface without one"),
))
}
fn is_timeout(e: &io::Error) -> bool {
matches!(
e.kind(),
io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut
)
}