From f31f5b86db1ac22c9de105d168a090996a204465 Mon Sep 17 00:00:00 2001 From: shellrow Date: Mon, 21 Jul 2025 00:09:56 +0900 Subject: [PATCH 1/2] Add socket options --- nex-socket/src/icmp/async_impl.rs | 62 ++++++++++++------- nex-socket/src/icmp/config.rs | 47 +++++++++++++-- nex-socket/src/icmp/sync_impl.rs | 60 +++++++++++++------ nex-socket/src/lib.rs | 44 ++++++++++++++ nex-socket/src/tcp/async_impl.rs | 30 +++++++++- nex-socket/src/tcp/config.rs | 79 ++++++++++++++++++++---- nex-socket/src/tcp/mod.rs | 5 ++ nex-socket/src/tcp/sync_impl.rs | 33 +++++++++-- nex-socket/src/udp/async_impl.rs | 24 ++++---- nex-socket/src/udp/config.rs | 99 ++++++++++++++++++++++++++++++- nex-socket/src/udp/sync_impl.rs | 33 +++++++---- 11 files changed, 426 insertions(+), 90 deletions(-) diff --git a/nex-socket/src/icmp/async_impl.rs b/nex-socket/src/icmp/async_impl.rs index 2c99567..4bc3fd6 100644 --- a/nex-socket/src/icmp/async_impl.rs +++ b/nex-socket/src/icmp/async_impl.rs @@ -1,4 +1,5 @@ use crate::icmp::{IcmpConfig, IcmpKind}; +use crate::SocketFamily; use socket2::{Domain, Protocol, Socket, Type as SockType}; use std::io; use std::net::{SocketAddr, UdpSocket as StdUdpSocket}; @@ -9,15 +10,15 @@ use tokio::net::UdpSocket; pub struct AsyncIcmpSocket { inner: UdpSocket, sock_type: SockType, - kind: IcmpKind, + socket_family: SocketFamily, } impl AsyncIcmpSocket { /// Create a new asynchronous ICMP socket. pub async fn new(config: &IcmpConfig) -> io::Result { - let (domain, proto) = match config.kind { - IcmpKind::V4 => (Domain::IPV4, Some(Protocol::ICMPV4)), - IcmpKind::V6 => (Domain::IPV6, Some(Protocol::ICMPV6)), + let (domain, proto) = match config.socket_family { + SocketFamily::IPV4 => (Domain::IPV4, Some(Protocol::ICMPV4)), + SocketFamily::IPV6 => (Domain::IPV6, Some(Protocol::ICMPV6)), }; // Build the socket with DGRAM preferred and RAW as a fallback @@ -35,9 +36,26 @@ impl AsyncIcmpSocket { socket.set_nonblocking(true)?; - // bind - if let Some(addr) = &config.bind { - socket.bind(&(*addr).into())?; + // TTL for IPv4 + if let Some(ttl) = config.ttl { + socket.set_ttl(ttl)?; + } + // Hop limit for IPv6 + if let Some(hoplimit) = config.hoplimit { + socket.set_unicast_hops_v6(hoplimit)?; + } + // Read timeout + if let Some(timeout) = config.read_timeout { + socket.set_read_timeout(Some(timeout))?; + } + // Write timeout + if let Some(timeout) = config.write_timeout { + socket.set_write_timeout(Some(timeout))?; + } + // FreeBSD only: optional FIB support + #[cfg(target_os = "freebsd")] + if let Some(fib) = config.fib { + socket.set_fib(fib)?; } // Linux: optional interface name @@ -46,15 +64,9 @@ impl AsyncIcmpSocket { socket.bind_device(Some(interface.as_bytes()))?; } - // TTL - if let Some(ttl) = config.ttl { - socket.set_ttl(ttl)?; - } - - // FreeBSD only: optional FIB support - #[cfg(target_os = "freebsd")] - if let Some(fib) = config.fib { - socket.set_fib(fib)?; + // bind + if let Some(addr) = &config.bind { + socket.bind(&(*addr).into())?; } let socket_type = socket.r#type()?; @@ -73,13 +85,13 @@ impl AsyncIcmpSocket { StdUdpSocket::from_raw_fd(socket.into_raw_fd()) }; - // std → tokio::net::UdpSocket + // std -> tokio::net::UdpSocket let inner = UdpSocket::from_std(std_socket)?; Ok(Self { inner, sock_type: socket_type, - kind: config.kind, + socket_family: config.socket_family, }) } @@ -103,9 +115,17 @@ impl AsyncIcmpSocket { self.sock_type } - /// Return the ICMP version. - pub fn kind(&self) -> IcmpKind { - self.kind + /// Return the socket family. + pub fn socket_family(&self) -> SocketFamily { + self.socket_family + } + + /// Return the ICMP kind. + pub fn icmp_kind(&self) -> IcmpKind { + match self.socket_family { + SocketFamily::IPV4 => IcmpKind::V4, + SocketFamily::IPV6 => IcmpKind::V6, + } } /// Access the native socket for low level operations. diff --git a/nex-socket/src/icmp/config.rs b/nex-socket/src/icmp/config.rs index 9e7d873..d195a11 100644 --- a/nex-socket/src/icmp/config.rs +++ b/nex-socket/src/icmp/config.rs @@ -1,5 +1,7 @@ use socket2::Type as SockType; -use std::net::SocketAddr; +use std::{net::SocketAddr, time::Duration}; + +use crate::SocketFamily; /// ICMP protocol version. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -11,23 +13,41 @@ pub enum IcmpKind { /// Configuration for an ICMP socket. #[derive(Debug, Clone)] pub struct IcmpConfig { - pub kind: IcmpKind, + /// The socket family. + pub socket_family: SocketFamily, + /// Optional bind address for the socket. pub bind: Option, + /// Time-to-live for IPv4 packets. pub ttl: Option, + /// Hop limit for IPv6 packets. + pub hoplimit: Option, + /// Read timeout for the socket. + pub read_timeout: Option, + /// Write timeout for the socket. + pub write_timeout: Option, + /// Network interface to use for the socket. pub interface: Option, + /// Socket type hint, DGRAM preferred on Linux, RAW fallback on macOS/Windows. pub sock_type_hint: SockType, + /// FreeBSD only: optional FIB (Forwarding Information Base) support. pub fib: Option, } impl IcmpConfig { pub fn new(kind: IcmpKind) -> Self { Self { - kind, + socket_family: match kind { + IcmpKind::V4 => SocketFamily::IPV4, + IcmpKind::V6 => SocketFamily::IPV6, + }, bind: None, ttl: None, + hoplimit: None, + read_timeout: None, + write_timeout: None, interface: None, - sock_type_hint: SockType::DGRAM, // DGRAM preferred on Linux, RAW fallback on macOS/Windows - fib: None, // FreeBSD only + sock_type_hint: SockType::DGRAM, + fib: None, } } @@ -41,6 +61,21 @@ impl IcmpConfig { self } + pub fn with_hoplimit(mut self, hops: u32) -> Self { + self.hoplimit = Some(hops); + self + } + + pub fn with_read_timeout(mut self, timeout: Duration) -> Self { + self.read_timeout = Some(timeout); + self + } + + pub fn with_write_timeout(mut self, timeout: Duration) -> Self { + self.write_timeout = Some(timeout); + self + } + pub fn with_interface(mut self, iface: impl Into) -> Self { self.interface = Some(iface.into()); self @@ -69,7 +104,7 @@ mod tests { .with_ttl(4) .with_interface("eth0") .with_sock_type(Type::RAW); - assert_eq!(cfg.kind, IcmpKind::V4); + assert_eq!(cfg.socket_family, SocketFamily::IPV4); assert_eq!(cfg.bind, Some(addr)); assert_eq!(cfg.ttl, Some(4)); assert_eq!(cfg.interface.as_deref(), Some("eth0")); diff --git a/nex-socket/src/icmp/sync_impl.rs b/nex-socket/src/icmp/sync_impl.rs index a9c28ba..2ee533f 100644 --- a/nex-socket/src/icmp/sync_impl.rs +++ b/nex-socket/src/icmp/sync_impl.rs @@ -1,4 +1,5 @@ use crate::icmp::{IcmpConfig, IcmpKind}; +use crate::SocketFamily; use socket2::{Domain, Protocol, Socket, Type as SockType}; use std::io; use std::net::{SocketAddr, UdpSocket}; @@ -8,15 +9,15 @@ use std::net::{SocketAddr, UdpSocket}; pub struct IcmpSocket { inner: UdpSocket, sock_type: SockType, - kind: IcmpKind, + socket_family: SocketFamily, } impl IcmpSocket { /// Create a new synchronous ICMP socket. pub fn new(config: &IcmpConfig) -> io::Result { - let (domain, proto) = match config.kind { - IcmpKind::V4 => (Domain::IPV4, Some(Protocol::ICMPV4)), - IcmpKind::V6 => (Domain::IPV6, Some(Protocol::ICMPV6)), + let (domain, proto) = match config.socket_family { + SocketFamily::IPV4 => (Domain::IPV4, Some(Protocol::ICMPV4)), + SocketFamily::IPV6 => (Domain::IPV6, Some(Protocol::ICMPV6)), }; let socket = match Socket::new(domain, config.sock_type_hint, proto) { @@ -31,25 +32,40 @@ impl IcmpSocket { } }; - socket.set_nonblocking(false)?; // blocking mode for sync usage - - if let Some(addr) = &config.bind { - socket.bind(&(*addr).into())?; - } - - #[cfg(any(target_os = "linux", target_os = "android", target_os = "fuchsia"))] - if let Some(interface) = &config.interface { - socket.bind_device(Some(interface.as_bytes()))?; - } + socket.set_nonblocking(false)?; + // TTL for IPv4 if let Some(ttl) = config.ttl { socket.set_ttl(ttl)?; } - + // Hop limit for IPv6 + if let Some(hoplimit) = config.hoplimit { + socket.set_unicast_hops_v6(hoplimit)?; + } + // Read timeout + if let Some(timeout) = config.read_timeout { + socket.set_read_timeout(Some(timeout))?; + } + // Write timeout + if let Some(timeout) = config.write_timeout { + socket.set_write_timeout(Some(timeout))?; + } + // FreeBSD only: optional FIB support #[cfg(target_os = "freebsd")] if let Some(fib) = config.fib { socket.set_fib(fib)?; } + + // Linux: optional interface name + #[cfg(any(target_os = "linux", target_os = "android", target_os = "fuchsia"))] + if let Some(interface) = &config.interface { + socket.bind_device(Some(interface.as_bytes()))?; + } + + // bind + if let Some(addr) = &config.bind { + socket.bind(&(*addr).into())?; + } // Convert socket2::Socket into std::net::UdpSocket let std_socket: UdpSocket = socket.into(); @@ -57,7 +73,7 @@ impl IcmpSocket { Ok(Self { inner: std_socket, sock_type: config.sock_type_hint, - kind: config.kind, + socket_family: config.socket_family, }) } @@ -81,9 +97,17 @@ impl IcmpSocket { self.sock_type } + /// Return the socket family. + pub fn socket_family(&self) -> SocketFamily { + self.socket_family + } + /// Return the ICMP variant. - pub fn kind(&self) -> IcmpKind { - self.kind + pub fn icmp_kind(&self) -> IcmpKind { + match self.socket_family { + SocketFamily::IPV4 => IcmpKind::V4, + SocketFamily::IPV6 => IcmpKind::V6, + } } /// Access the underlying socket. diff --git a/nex-socket/src/lib.rs b/nex-socket/src/lib.rs index bad1362..49be7a5 100644 --- a/nex-socket/src/lib.rs +++ b/nex-socket/src/lib.rs @@ -7,3 +7,47 @@ pub mod icmp; pub mod tcp; pub mod udp; + +use std::net::{IpAddr, SocketAddr}; + +/// Represents the socket address family (IPv4 or IPv6) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SocketFamily { + IPV4, + IPV6, +} + +impl SocketFamily { + /// Returns the socket family of the IP address. + pub fn from_ip(ip: &IpAddr) -> Self { + match ip { + IpAddr::V4(_) => SocketFamily::IPV4, + IpAddr::V6(_) => SocketFamily::IPV6, + } + } + + /// Returns the socket family of the socket address. + pub fn from_socket_addr(addr: &SocketAddr) -> Self { + match addr { + SocketAddr::V4(_) => SocketFamily::IPV4, + SocketAddr::V6(_) => SocketFamily::IPV6, + } + } + + /// Returns true if the socket family is IPv4. + pub fn is_v4(&self) -> bool { + matches!(self, SocketFamily::IPV4) + } + + /// Returns true if the socket family is IPv6. + pub fn is_v6(&self) -> bool { + matches!(self, SocketFamily::IPV6) + } + + pub(crate) fn to_domain(&self) -> socket2::Domain { + match self { + SocketFamily::IPV4 => socket2::Domain::IPV4, + SocketFamily::IPV6 => socket2::Domain::IPV6, + } + } +} diff --git a/nex-socket/src/tcp/async_impl.rs b/nex-socket/src/tcp/async_impl.rs index 42a073a..47a5993 100644 --- a/nex-socket/src/tcp/async_impl.rs +++ b/nex-socket/src/tcp/async_impl.rs @@ -14,8 +14,10 @@ pub struct AsyncTcpSocket { impl AsyncTcpSocket { /// Create a socket from the given configuration without connecting. pub fn from_config(config: &TcpConfig) -> io::Result { - let socket = Socket::new(config.domain, config.sock_type, Some(Protocol::TCP))?; + let socket = Socket::new(config.socket_family.to_domain(), config.socket_type.to_sock_type(), Some(Protocol::TCP))?; + socket.set_nonblocking(true)?; + if let Some(flag) = config.reuseaddr { socket.set_reuse_address(flag)?; } @@ -25,6 +27,18 @@ impl AsyncTcpSocket { if let Some(ttl) = config.ttl { socket.set_ttl(ttl)?; } + if let Some(hoplimit) = config.hoplimit { + socket.set_unicast_hops_v6(hoplimit)?; + } + if let Some(keepalive) = config.keepalive { + socket.set_keepalive(keepalive)?; + } + if let Some(timeout) = config.read_timeout { + socket.set_read_timeout(Some(timeout))?; + } + if let Some(timeout) = config.write_timeout { + socket.set_write_timeout(Some(timeout))?; + } #[cfg(any(target_os = "linux", target_os = "android", target_os = "fuchsia"))] if let Some(iface) = &config.bind_device { @@ -35,8 +49,6 @@ impl AsyncTcpSocket { socket.bind(&addr.into())?; } - socket.set_nonblocking(true)?; - Ok(Self { socket }) } @@ -145,6 +157,10 @@ impl AsyncTcpSocket { Ok((n, addr)) } + pub fn shutdown(&self, how: std::net::Shutdown) -> io::Result<()> { + self.socket.shutdown(how) + } + // --- option helpers --- pub fn set_reuseaddr(&self, on: bool) -> io::Result<()> { @@ -163,6 +179,14 @@ impl AsyncTcpSocket { self.socket.set_ttl(ttl) } + pub fn set_hoplimit(&self, hops: u32) -> io::Result<()> { + self.socket.set_unicast_hops_v6(hops) + } + + pub fn set_keepalive(&self, on: bool) -> io::Result<()> { + self.socket.set_keepalive(on) + } + pub fn set_bind_device(&self, iface: &str) -> io::Result<()> { #[cfg(any(target_os = "linux", target_os = "android", target_os = "fuchsia"))] return self.socket.bind_device(Some(iface.as_bytes())); diff --git a/nex-socket/src/tcp/config.rs b/nex-socket/src/tcp/config.rs index 8946994..a7ef623 100644 --- a/nex-socket/src/tcp/config.rs +++ b/nex-socket/src/tcp/config.rs @@ -1,42 +1,75 @@ -use socket2::{Domain, Type as SockType}; +use socket2::Type as SockType; use std::net::SocketAddr; use std::time::Duration; +use crate::SocketFamily; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TcpSocketType { + Stream, + Raw, +} + +impl TcpSocketType { + pub fn is_stream(&self) -> bool { + matches!(self, TcpSocketType::Stream) + } + + pub fn is_raw(&self) -> bool { + matches!(self, TcpSocketType::Raw) + } + + pub(crate) fn to_sock_type(&self) -> SockType { + match self { + TcpSocketType::Stream => SockType::STREAM, + TcpSocketType::Raw => SockType::RAW, + } + } +} + /// Configuration options for a TCP socket. #[derive(Debug, Clone)] pub struct TcpConfig { - pub domain: Domain, - pub sock_type: SockType, + pub socket_family: SocketFamily, + pub socket_type: TcpSocketType, pub bind_addr: Option, pub nonblocking: bool, pub reuseaddr: Option, pub nodelay: Option, pub linger: Option, pub ttl: Option, + pub hoplimit: Option, + pub read_timeout: Option, + pub write_timeout: Option, pub bind_device: Option, + pub keepalive: Option, } impl TcpConfig { /// Create a STREAM socket for IPv4. pub fn v4_stream() -> Self { Self { - domain: Domain::IPV4, - sock_type: SockType::STREAM, + socket_family: SocketFamily::IPV4, + socket_type: TcpSocketType::Stream, bind_addr: None, nonblocking: false, reuseaddr: None, nodelay: None, linger: None, ttl: None, + hoplimit: None, + read_timeout: None, + write_timeout: None, bind_device: None, + keepalive: None, } } /// Create a RAW socket. Requires administrator privileges. pub fn raw_v4() -> Self { Self { - domain: Domain::IPV4, - sock_type: SockType::RAW, + socket_family: SocketFamily::IPV4, + socket_type: TcpSocketType::Raw, ..Self::v4_stream() } } @@ -44,8 +77,8 @@ impl TcpConfig { /// Create a STREAM socket for IPv6. pub fn v6_stream() -> Self { Self { - domain: Domain::IPV6, - sock_type: SockType::STREAM, + socket_family: SocketFamily::IPV6, + socket_type: TcpSocketType::Stream, ..Self::v4_stream() } } @@ -53,8 +86,8 @@ impl TcpConfig { /// Create a RAW socket for IPv6. Requires administrator privileges. pub fn raw_v6() -> Self { Self { - domain: Domain::IPV6, - sock_type: SockType::RAW, + socket_family: SocketFamily::IPV6, + socket_type: TcpSocketType::Raw, ..Self::v4_stream() } } @@ -91,6 +124,26 @@ impl TcpConfig { self } + pub fn with_hoplimit(mut self, hops: u32) -> Self { + self.hoplimit = Some(hops); + self + } + + pub fn with_keepalive(mut self, on: bool) -> Self { + self.keepalive = Some(on); + self + } + + pub fn with_read_timeout(mut self, timeout: Duration) -> Self { + self.read_timeout = Some(timeout); + self + } + + pub fn with_write_timeout(mut self, timeout: Duration) -> Self { + self.write_timeout = Some(timeout); + self + } + pub fn with_bind_device(mut self, iface: impl Into) -> Self { self.bind_device = Some(iface.into()); self @@ -111,8 +164,8 @@ mod tests { .with_nodelay(true) .with_ttl(10); - assert_eq!(cfg.domain, Domain::IPV4); - assert_eq!(cfg.sock_type, SockType::STREAM); + assert_eq!(cfg.socket_family, SocketFamily::IPV4); + assert_eq!(cfg.socket_type, TcpSocketType::Stream); assert_eq!(cfg.bind_addr, Some(addr)); assert!(cfg.nonblocking); assert_eq!(cfg.reuseaddr, Some(true)); diff --git a/nex-socket/src/tcp/mod.rs b/nex-socket/src/tcp/mod.rs index 4cc664c..318f438 100644 --- a/nex-socket/src/tcp/mod.rs +++ b/nex-socket/src/tcp/mod.rs @@ -5,3 +5,8 @@ mod sync_impl; pub use async_impl::*; pub use config::*; pub use sync_impl::*; + +pub enum TcpSocketType { + Stream, + Raw, +} diff --git a/nex-socket/src/tcp/sync_impl.rs b/nex-socket/src/tcp/sync_impl.rs index 725c8b1..eaec8af 100644 --- a/nex-socket/src/tcp/sync_impl.rs +++ b/nex-socket/src/tcp/sync_impl.rs @@ -20,7 +20,9 @@ pub struct TcpSocket { impl TcpSocket { /// Build a socket according to `TcpSocketConfig`. pub fn from_config(config: &TcpConfig) -> io::Result { - let socket = Socket::new(config.domain, config.sock_type, Some(Protocol::TCP))?; + let socket = Socket::new(config.socket_family.to_domain(), config.socket_type.to_sock_type(), Some(Protocol::TCP))?; + + socket.set_nonblocking(config.nonblocking)?; // Apply all configuration options if let Some(flag) = config.reuseaddr { @@ -35,6 +37,18 @@ impl TcpSocket { if let Some(ttl) = config.ttl { socket.set_ttl(ttl)?; } + if let Some(hoplimit) = config.hoplimit { + socket.set_unicast_hops_v6(hoplimit)?; + } + if let Some(keepalive) = config.keepalive { + socket.set_keepalive(keepalive)?; + } + if let Some(timeout) = config.read_timeout { + socket.set_read_timeout(Some(timeout))?; + } + if let Some(timeout) = config.write_timeout { + socket.set_write_timeout(Some(timeout))?; + } #[cfg(any(target_os = "linux", target_os = "android", target_os = "fuchsia"))] if let Some(iface) = &config.bind_device { @@ -46,9 +60,6 @@ impl TcpSocket { socket.bind(&addr.into())?; } - // Set non blocking mode - socket.set_nonblocking(config.nonblocking)?; - Ok(Self { socket }) } @@ -238,6 +249,10 @@ impl TcpSocket { Ok((n, addr)) } + pub fn shutdown(&self, how: std::net::Shutdown) -> io::Result<()> { + self.socket.shutdown(how) + } + // --- option helpers --- pub fn set_reuseaddr(&self, on: bool) -> io::Result<()> { @@ -256,6 +271,14 @@ impl TcpSocket { self.socket.set_ttl(ttl) } + pub fn set_hoplimit(&self, hops: u32) -> io::Result<()> { + self.socket.set_unicast_hops_v6(hops) + } + + pub fn set_keepalive(&self, on: bool) -> io::Result<()> { + self.socket.set_keepalive(on) + } + pub fn set_bind_device(&self, iface: &str) -> io::Result<()> { #[cfg(any(target_os = "linux", target_os = "android", target_os = "fuchsia"))] return self.socket.bind_device(Some(iface.as_bytes())); @@ -270,8 +293,6 @@ impl TcpSocket { } } - // --- information helpers --- - pub fn local_addr(&self) -> io::Result { self.socket .local_addr()? diff --git a/nex-socket/src/udp/async_impl.rs b/nex-socket/src/udp/async_impl.rs index 29543a8..15631fc 100644 --- a/nex-socket/src/udp/async_impl.rs +++ b/nex-socket/src/udp/async_impl.rs @@ -13,26 +13,28 @@ pub struct AsyncUdpSocket { impl AsyncUdpSocket { /// Create an asynchronous UDP socket from the given configuration. pub fn from_config(config: &UdpConfig) -> io::Result { - // Determine address family from the bind address - let domain = match config.bind_addr { - Some(SocketAddr::V4(_)) => Domain::IPV4, - Some(SocketAddr::V6(_)) => Domain::IPV6, - None => Domain::IPV4, // default - }; - - let socket = Socket::new(domain, SockType::DGRAM, Some(Protocol::UDP))?; + let socket = Socket::new(config.socket_family.to_domain(), config.socket_type.to_sock_type(), Some(Protocol::UDP))?; + socket.set_nonblocking(true)?; + if let Some(flag) = config.reuseaddr { socket.set_reuse_address(flag)?; } - if let Some(flag) = config.broadcast { socket.set_broadcast(flag)?; } - if let Some(ttl) = config.ttl { socket.set_ttl(ttl)?; } + if let Some(hoplimit) = config.hoplimit { + socket.set_unicast_hops_v6(hoplimit)?; + } + if let Some(timeout) = config.read_timeout { + socket.set_read_timeout(Some(timeout))?; + } + if let Some(timeout) = config.write_timeout { + socket.set_write_timeout(Some(timeout))?; + } #[cfg(any(target_os = "linux", target_os = "android", target_os = "fuchsia"))] if let Some(iface) = &config.bind_device { @@ -43,8 +45,6 @@ impl AsyncUdpSocket { socket.bind(&addr.into())?; } - socket.set_nonblocking(true)?; - #[cfg(windows)] let std_socket = unsafe { use std::os::windows::io::{FromRawSocket, IntoRawSocket}; diff --git a/nex-socket/src/udp/config.rs b/nex-socket/src/udp/config.rs index ce1a008..a558017 100644 --- a/nex-socket/src/udp/config.rs +++ b/nex-socket/src/udp/config.rs @@ -1,8 +1,39 @@ -use std::net::SocketAddr; +use std::{net::SocketAddr, time::Duration}; + +use socket2::Type as SockType; + +use crate::SocketFamily; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UdpSocketType { + Dgram, + Raw, +} + +impl UdpSocketType { + pub fn is_dgram(&self) -> bool { + matches!(self, UdpSocketType::Dgram) + } + + pub fn is_raw(&self) -> bool { + matches!(self, UdpSocketType::Raw) + } + + pub(crate) fn to_sock_type(&self) -> SockType { + match self { + UdpSocketType::Dgram => SockType::DGRAM, + UdpSocketType::Raw => SockType::RAW, + } + } +} /// Configuration options for a UDP socket. #[derive(Debug, Clone)] pub struct UdpConfig { + /// The socket family. + pub socket_family: SocketFamily, + /// The socket type (DGRAM or RAW). + pub socket_type: UdpSocketType, /// Address to bind. If `None`, the operating system chooses the address. pub bind_addr: Option, @@ -15,6 +46,12 @@ pub struct UdpConfig { /// Time to live value. pub ttl: Option, + /// Hop limit value. + pub hoplimit: Option, + + pub read_timeout: Option, + pub write_timeout: Option, + /// Bind to a specific interface (Linux only). pub bind_device: Option, } @@ -22,15 +59,75 @@ pub struct UdpConfig { impl Default for UdpConfig { fn default() -> Self { Self { + socket_family: SocketFamily::IPV4, + socket_type: UdpSocketType::Dgram, bind_addr: None, reuseaddr: None, broadcast: None, ttl: None, + hoplimit: None, + read_timeout: None, + write_timeout: None, bind_device: None, } } } +impl UdpConfig { + /// Create a new UDP configuration with default values. + pub fn new() -> Self { + Self::default() + } + + /// Set the bind address. + pub fn with_bind_addr(mut self, addr: SocketAddr) -> Self { + self.bind_addr = Some(addr); + self + } + + /// Enable address reuse. + pub fn with_reuseaddr(mut self, on: bool) -> Self { + self.reuseaddr = Some(on); + self + } + + /// Allow broadcast. + pub fn with_broadcast(mut self, on: bool) -> Self { + self.broadcast = Some(on); + self + } + + /// Set the time to live value. + pub fn with_ttl(mut self, ttl: u32) -> Self { + self.ttl = Some(ttl); + self + } + + /// Set the hop limit value. + pub fn with_hoplimit(mut self, hops: u32) -> Self { + self.hoplimit = Some(hops); + self + } + + /// Set the read timeout. + pub fn with_read_timeout(mut self, timeout: Duration) -> Self { + self.read_timeout = Some(timeout); + self + } + + /// Set the write timeout. + pub fn with_write_timeout(mut self, timeout: Duration) -> Self { + self.write_timeout = Some(timeout); + self + } + + /// Bind to a specific interface (Linux only). + pub fn with_bind_device(mut self, iface: impl Into) -> Self { + self.bind_device = Some(iface.into()); + self + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/nex-socket/src/udp/sync_impl.rs b/nex-socket/src/udp/sync_impl.rs index aa6ea0a..c687ee6 100644 --- a/nex-socket/src/udp/sync_impl.rs +++ b/nex-socket/src/udp/sync_impl.rs @@ -12,26 +12,28 @@ pub struct UdpSocket { impl UdpSocket { /// Create a socket from the provided configuration. pub fn from_config(config: &UdpConfig) -> io::Result { - // Determine address family from the bind address - let domain = match config.bind_addr { - Some(SocketAddr::V4(_)) => Domain::IPV4, - Some(SocketAddr::V6(_)) => Domain::IPV6, - None => Domain::IPV4, // default - }; + let socket = Socket::new(config.socket_family.to_domain(), config.socket_type.to_sock_type(), Some(Protocol::UDP))?; - let socket = Socket::new(domain, SockType::DGRAM, Some(Protocol::UDP))?; + socket.set_nonblocking(false)?; if let Some(flag) = config.reuseaddr { socket.set_reuse_address(flag)?; } - if let Some(flag) = config.broadcast { socket.set_broadcast(flag)?; } - if let Some(ttl) = config.ttl { socket.set_ttl(ttl)?; } + if let Some(hoplimit) = config.hoplimit { + socket.set_unicast_hops_v6(hoplimit)?; + } + if let Some(timeout) = config.read_timeout { + socket.set_read_timeout(Some(timeout))?; + } + if let Some(timeout) = config.write_timeout { + socket.set_write_timeout(Some(timeout))?; + } #[cfg(any(target_os = "linux", target_os = "android", target_os = "fuchsia"))] if let Some(iface) = &config.bind_device { @@ -42,7 +44,6 @@ impl UdpSocket { socket.bind(&addr.into())?; } - socket.set_nonblocking(false)?; // blocking mode for sync usage Ok(Self { socket }) } @@ -96,6 +97,18 @@ impl UdpSocket { Ok((n, addr)) } + pub fn set_ttl(&self, ttl: u32) -> io::Result<()> { + self.socket.set_ttl(ttl) + } + + pub fn set_hoplimit(&self, hops: u32) -> io::Result<()> { + self.socket.set_unicast_hops_v6(hops) + } + + pub fn set_keepalive(&self, on: bool) -> io::Result<()> { + self.socket.set_keepalive(on) + } + /// Retrieve the local socket address. pub fn local_addr(&self) -> io::Result { self.socket From be067ec05ef3151719d952d2d740dd0d636a1435 Mon Sep 17 00:00:00 2001 From: shellrow Date: Mon, 21 Jul 2025 09:48:50 +0900 Subject: [PATCH 2/2] Add docs --- nex-socket/src/icmp/async_impl.rs | 27 +++++++-------- nex-socket/src/icmp/config.rs | 56 +++++++++++++++++++++++++++---- nex-socket/src/icmp/sync_impl.rs | 27 +++++++-------- nex-socket/src/lib.rs | 1 + nex-socket/src/tcp/async_impl.rs | 15 +++++++-- nex-socket/src/tcp/config.rs | 17 ++++++++++ nex-socket/src/tcp/mod.rs | 5 --- nex-socket/src/tcp/sync_impl.rs | 27 +++++++++++---- nex-socket/src/udp/async_impl.rs | 3 ++ nex-socket/src/udp/config.rs | 12 +++---- nex-socket/src/udp/sync_impl.rs | 3 ++ 11 files changed, 139 insertions(+), 54 deletions(-) diff --git a/nex-socket/src/icmp/async_impl.rs b/nex-socket/src/icmp/async_impl.rs index 4bc3fd6..e56bf06 100644 --- a/nex-socket/src/icmp/async_impl.rs +++ b/nex-socket/src/icmp/async_impl.rs @@ -1,4 +1,4 @@ -use crate::icmp::{IcmpConfig, IcmpKind}; +use crate::icmp::{IcmpConfig, IcmpKind, IcmpSocketType}; use crate::SocketFamily; use socket2::{Domain, Protocol, Socket, Type as SockType}; use std::io; @@ -9,7 +9,7 @@ use tokio::net::UdpSocket; #[derive(Debug)] pub struct AsyncIcmpSocket { inner: UdpSocket, - sock_type: SockType, + socket_type: IcmpSocketType, socket_family: SocketFamily, } @@ -22,10 +22,10 @@ impl AsyncIcmpSocket { }; // Build the socket with DGRAM preferred and RAW as a fallback - let socket = match Socket::new(domain, config.sock_type_hint, proto) { + let socket = match Socket::new(domain, config.sock_type_hint.to_sock_type(), proto) { Ok(s) => s, Err(_) => { - let alt_type = if config.sock_type_hint == SockType::DGRAM { + let alt_type = if config.sock_type_hint.is_dgram() { SockType::RAW } else { SockType::DGRAM @@ -36,19 +36,16 @@ impl AsyncIcmpSocket { socket.set_nonblocking(true)?; - // TTL for IPv4 + // Set socket options based on configuration if let Some(ttl) = config.ttl { socket.set_ttl(ttl)?; } - // Hop limit for IPv6 if let Some(hoplimit) = config.hoplimit { socket.set_unicast_hops_v6(hoplimit)?; } - // Read timeout if let Some(timeout) = config.read_timeout { socket.set_read_timeout(Some(timeout))?; } - // Write timeout if let Some(timeout) = config.write_timeout { socket.set_write_timeout(Some(timeout))?; } @@ -57,19 +54,18 @@ impl AsyncIcmpSocket { if let Some(fib) = config.fib { socket.set_fib(fib)?; } - // Linux: optional interface name #[cfg(any(target_os = "linux", target_os = "android", target_os = "fuchsia"))] if let Some(interface) = &config.interface { socket.bind_device(Some(interface.as_bytes()))?; } - // bind + // bind to the specified address if provided if let Some(addr) = &config.bind { socket.bind(&(*addr).into())?; } - let socket_type = socket.r#type()?; + let sock_type = socket.r#type()?; // Convert socket2::Socket into std::net::UdpSocket #[cfg(windows)] @@ -90,7 +86,7 @@ impl AsyncIcmpSocket { Ok(Self { inner, - sock_type: socket_type, + socket_type: IcmpSocketType::from_sock_type(sock_type), socket_family: config.socket_family, }) } @@ -111,8 +107,8 @@ impl AsyncIcmpSocket { } /// Return the socket type (DGRAM or RAW). - pub fn sock_type(&self) -> SockType { - self.sock_type + pub fn socket_type(&self) -> IcmpSocketType { + self.socket_type } /// Return the socket family. @@ -128,13 +124,14 @@ impl AsyncIcmpSocket { } } - /// Access the native socket for low level operations. + /// Extract the RAW file descriptor for Unix. #[cfg(unix)] pub fn as_raw_fd(&self) -> std::os::unix::io::RawFd { use std::os::fd::AsRawFd; self.inner.as_raw_fd() } + /// Extract the RAW socket handle for Windows. #[cfg(windows)] pub fn as_raw_socket(&self) -> std::os::windows::io::RawSocket { use std::os::windows::io::AsRawSocket; diff --git a/nex-socket/src/icmp/config.rs b/nex-socket/src/icmp/config.rs index d195a11..3583f29 100644 --- a/nex-socket/src/icmp/config.rs +++ b/nex-socket/src/icmp/config.rs @@ -10,6 +10,42 @@ pub enum IcmpKind { V6, } +/// ICMP socket type, either DGRAM or RAW. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IcmpSocketType { + Dgram, + Raw, +} + +impl IcmpSocketType { + /// Returns true if the socket type is DGRAM. + pub fn is_dgram(&self) -> bool { + matches!(self, IcmpSocketType::Dgram) + } + + /// Returns true if the socket type is RAW. + pub fn is_raw(&self) -> bool { + matches!(self, IcmpSocketType::Raw) + } + + /// Converts the ICMP socket type from a `socket2::Type`. + pub(crate) fn from_sock_type(sock_type: SockType) -> Self { + match sock_type { + SockType::DGRAM => IcmpSocketType::Dgram, + SockType::RAW => IcmpSocketType::Raw, + _ => panic!("Invalid ICMP socket type"), + } + } + + /// Converts the ICMP socket type to a `socket2::Type`. + pub(crate) fn to_sock_type(&self) -> SockType { + match self { + IcmpSocketType::Dgram => SockType::DGRAM, + IcmpSocketType::Raw => SockType::RAW, + } + } +} + /// Configuration for an ICMP socket. #[derive(Debug, Clone)] pub struct IcmpConfig { @@ -28,12 +64,13 @@ pub struct IcmpConfig { /// Network interface to use for the socket. pub interface: Option, /// Socket type hint, DGRAM preferred on Linux, RAW fallback on macOS/Windows. - pub sock_type_hint: SockType, + pub sock_type_hint: IcmpSocketType, /// FreeBSD only: optional FIB (Forwarding Information Base) support. pub fib: Option, } impl IcmpConfig { + /// Creates a new ICMP configuration with the specified kind. pub fn new(kind: IcmpKind) -> Self { Self { socket_family: match kind { @@ -46,46 +83,54 @@ impl IcmpConfig { read_timeout: None, write_timeout: None, interface: None, - sock_type_hint: SockType::DGRAM, + sock_type_hint: IcmpSocketType::Dgram, fib: None, } } + /// Set bind address for the socket. pub fn with_bind(mut self, addr: SocketAddr) -> Self { self.bind = Some(addr); self } + /// Set the time-to-live for IPv4 packets. pub fn with_ttl(mut self, ttl: u32) -> Self { self.ttl = Some(ttl); self } + /// Set the hop limit for IPv6 packets. pub fn with_hoplimit(mut self, hops: u32) -> Self { self.hoplimit = Some(hops); self } + /// Set the read timeout for the socket. pub fn with_read_timeout(mut self, timeout: Duration) -> Self { self.read_timeout = Some(timeout); self } + /// Set the write timeout for the socket. pub fn with_write_timeout(mut self, timeout: Duration) -> Self { self.write_timeout = Some(timeout); self } + /// Set the network interface to use for the socket. pub fn with_interface(mut self, iface: impl Into) -> Self { self.interface = Some(iface.into()); self } - pub fn with_sock_type(mut self, ty: SockType) -> Self { + /// Set the socket type hint. (DGRAM or RAW) + pub fn with_sock_type(mut self, ty: IcmpSocketType) -> Self { self.sock_type_hint = ty; self } + /// Set the FIB (Forwarding Information Base) for FreeBSD. pub fn with_fib(mut self, fib: u32) -> Self { self.fib = Some(fib); self @@ -95,7 +140,6 @@ impl IcmpConfig { #[cfg(test)] mod tests { use super::*; - use socket2::Type; #[test] fn icmp_config_builders() { let addr: SocketAddr = "127.0.0.1:0".parse().unwrap(); @@ -103,11 +147,11 @@ mod tests { .with_bind(addr) .with_ttl(4) .with_interface("eth0") - .with_sock_type(Type::RAW); + .with_sock_type(IcmpSocketType::Raw); assert_eq!(cfg.socket_family, SocketFamily::IPV4); assert_eq!(cfg.bind, Some(addr)); assert_eq!(cfg.ttl, Some(4)); assert_eq!(cfg.interface.as_deref(), Some("eth0")); - assert_eq!(cfg.sock_type_hint, Type::RAW); + assert_eq!(cfg.sock_type_hint, IcmpSocketType::Raw); } } diff --git a/nex-socket/src/icmp/sync_impl.rs b/nex-socket/src/icmp/sync_impl.rs index 2ee533f..0c028c0 100644 --- a/nex-socket/src/icmp/sync_impl.rs +++ b/nex-socket/src/icmp/sync_impl.rs @@ -1,4 +1,4 @@ -use crate::icmp::{IcmpConfig, IcmpKind}; +use crate::icmp::{IcmpConfig, IcmpKind, IcmpSocketType}; use crate::SocketFamily; use socket2::{Domain, Protocol, Socket, Type as SockType}; use std::io; @@ -8,7 +8,7 @@ use std::net::{SocketAddr, UdpSocket}; #[derive(Debug)] pub struct IcmpSocket { inner: UdpSocket, - sock_type: SockType, + socket_type: IcmpSocketType, socket_family: SocketFamily, } @@ -20,10 +20,10 @@ impl IcmpSocket { SocketFamily::IPV6 => (Domain::IPV6, Some(Protocol::ICMPV6)), }; - let socket = match Socket::new(domain, config.sock_type_hint, proto) { + let socket = match Socket::new(domain, config.sock_type_hint.to_sock_type(), proto) { Ok(s) => s, Err(_) => { - let alt_type = if config.sock_type_hint == SockType::DGRAM { + let alt_type = if config.sock_type_hint.is_dgram() { SockType::RAW } else { SockType::DGRAM @@ -34,19 +34,16 @@ impl IcmpSocket { socket.set_nonblocking(false)?; - // TTL for IPv4 + // Set socket options based on configuration if let Some(ttl) = config.ttl { socket.set_ttl(ttl)?; } - // Hop limit for IPv6 if let Some(hoplimit) = config.hoplimit { socket.set_unicast_hops_v6(hoplimit)?; } - // Read timeout if let Some(timeout) = config.read_timeout { socket.set_read_timeout(Some(timeout))?; } - // Write timeout if let Some(timeout) = config.write_timeout { socket.set_write_timeout(Some(timeout))?; } @@ -55,24 +52,25 @@ impl IcmpSocket { if let Some(fib) = config.fib { socket.set_fib(fib)?; } - // Linux: optional interface name #[cfg(any(target_os = "linux", target_os = "android", target_os = "fuchsia"))] if let Some(interface) = &config.interface { socket.bind_device(Some(interface.as_bytes()))?; } - // bind + // bind to the specified address if provided if let Some(addr) = &config.bind { socket.bind(&(*addr).into())?; } + let sock_type = socket.r#type()?; + // Convert socket2::Socket into std::net::UdpSocket let std_socket: UdpSocket = socket.into(); Ok(Self { inner: std_socket, - sock_type: config.sock_type_hint, + socket_type: IcmpSocketType::from_sock_type(sock_type), socket_family: config.socket_family, }) } @@ -93,8 +91,8 @@ impl IcmpSocket { } /// Return the socket type. - pub fn sock_type(&self) -> SockType { - self.sock_type + pub fn socket_type(&self) -> IcmpSocketType { + self.socket_type } /// Return the socket family. @@ -110,13 +108,14 @@ impl IcmpSocket { } } - /// Access the underlying socket. + /// Extract the RAW file descriptor for Unix. #[cfg(unix)] pub fn as_raw_fd(&self) -> std::os::unix::io::RawFd { use std::os::fd::AsRawFd; self.inner.as_raw_fd() } + /// Extract the RAW socket handle for Windows. #[cfg(windows)] pub fn as_raw_socket(&self) -> std::os::windows::io::RawSocket { use std::os::windows::io::AsRawSocket; diff --git a/nex-socket/src/lib.rs b/nex-socket/src/lib.rs index 49be7a5..4849a8b 100644 --- a/nex-socket/src/lib.rs +++ b/nex-socket/src/lib.rs @@ -44,6 +44,7 @@ impl SocketFamily { matches!(self, SocketFamily::IPV6) } + /// Converts the socket family to a `socket2::Domain`. pub(crate) fn to_domain(&self) -> socket2::Domain { match self { SocketFamily::IPV4 => socket2::Domain::IPV4, diff --git a/nex-socket/src/tcp/async_impl.rs b/nex-socket/src/tcp/async_impl.rs index 47a5993..41c489a 100644 --- a/nex-socket/src/tcp/async_impl.rs +++ b/nex-socket/src/tcp/async_impl.rs @@ -18,6 +18,7 @@ impl AsyncTcpSocket { socket.set_nonblocking(true)?; + // Set socket options based on configuration if let Some(flag) = config.reuseaddr { socket.set_reuse_address(flag)?; } @@ -40,11 +41,13 @@ impl AsyncTcpSocket { socket.set_write_timeout(Some(timeout))?; } + // Linux: optional interface name #[cfg(any(target_os = "linux", target_os = "android", target_os = "fuchsia"))] if let Some(iface) = &config.bind_device { socket.bind_device(Some(iface.as_bytes()))?; } + // bind to the specified address if provided if let Some(addr) = config.bind_addr { socket.bind(&addr.into())?; } @@ -157,36 +160,42 @@ impl AsyncTcpSocket { Ok((n, addr)) } + /// Shutdown the socket. pub fn shutdown(&self, how: std::net::Shutdown) -> io::Result<()> { self.socket.shutdown(how) } - // --- option helpers --- - + /// Set reuse address option. pub fn set_reuseaddr(&self, on: bool) -> io::Result<()> { self.socket.set_reuse_address(on) } + /// Set no delay option for TCP. pub fn set_nodelay(&self, on: bool) -> io::Result<()> { self.socket.set_nodelay(on) } + /// Set linger option for the socket. pub fn set_linger(&self, dur: Option) -> io::Result<()> { self.socket.set_linger(dur) } + /// Set the time-to-live for IPv4 packets. pub fn set_ttl(&self, ttl: u32) -> io::Result<()> { self.socket.set_ttl(ttl) } + /// Set the hop limit for IPv6 packets. pub fn set_hoplimit(&self, hops: u32) -> io::Result<()> { self.socket.set_unicast_hops_v6(hops) } + /// Set the keepalive option for the socket. pub fn set_keepalive(&self, on: bool) -> io::Result<()> { self.socket.set_keepalive(on) } + /// Set the bind device for the socket (Linux specific). pub fn set_bind_device(&self, iface: &str) -> io::Result<()> { #[cfg(any(target_os = "linux", target_os = "android", target_os = "fuchsia"))] return self.socket.bind_device(Some(iface.as_bytes())); @@ -215,12 +224,14 @@ impl AsyncTcpSocket { TcpStream::from_std(std_stream) } + /// Extract the RAW file descriptor for Unix. #[cfg(unix)] pub fn as_raw_fd(&self) -> std::os::unix::io::RawFd { use std::os::fd::AsRawFd; self.socket.as_raw_fd() } + /// Extract the RAW socket handle for Windows. #[cfg(windows)] pub fn as_raw_socket(&self) -> std::os::windows::io::RawSocket { use std::os::windows::io::AsRawSocket; diff --git a/nex-socket/src/tcp/config.rs b/nex-socket/src/tcp/config.rs index a7ef623..a836709 100644 --- a/nex-socket/src/tcp/config.rs +++ b/nex-socket/src/tcp/config.rs @@ -4,6 +4,7 @@ use std::time::Duration; use crate::SocketFamily; +/// TCP socket type, either STREAM or RAW. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TcpSocketType { Stream, @@ -11,14 +12,17 @@ pub enum TcpSocketType { } impl TcpSocketType { + /// Returns true if the socket type is STREAM. pub fn is_stream(&self) -> bool { matches!(self, TcpSocketType::Stream) } + /// Returns true if the socket type is RAW. pub fn is_raw(&self) -> bool { matches!(self, TcpSocketType::Raw) } + /// Converts the TCP socket type to a `socket2::Type`. pub(crate) fn to_sock_type(&self) -> SockType { match self { TcpSocketType::Stream => SockType::STREAM, @@ -30,18 +34,31 @@ impl TcpSocketType { /// Configuration options for a TCP socket. #[derive(Debug, Clone)] pub struct TcpConfig { + /// The socket family, either IPv4 or IPv6. pub socket_family: SocketFamily, + /// The type of TCP socket, either STREAM or RAW. pub socket_type: TcpSocketType, + /// Optional address to bind the socket to. pub bind_addr: Option, + /// Whether the socket should be non-blocking. pub nonblocking: bool, + /// Whether to allow address reuse. pub reuseaddr: Option, + /// Whether to disable Nagle's algorithm (TCP_NODELAY). pub nodelay: Option, + /// Optional linger duration for the socket. pub linger: Option, + /// Optional Time-To-Live (TTL) for the socket. pub ttl: Option, + /// Optional Hop Limit for the socket (IPv6). pub hoplimit: Option, + /// Optional read timeout for the socket. pub read_timeout: Option, + /// Optional write timeout for the socket. pub write_timeout: Option, + /// Optional device to bind the socket to. pub bind_device: Option, + /// Whether to enable TCP keepalive. pub keepalive: Option, } diff --git a/nex-socket/src/tcp/mod.rs b/nex-socket/src/tcp/mod.rs index 318f438..4cc664c 100644 --- a/nex-socket/src/tcp/mod.rs +++ b/nex-socket/src/tcp/mod.rs @@ -5,8 +5,3 @@ mod sync_impl; pub use async_impl::*; pub use config::*; pub use sync_impl::*; - -pub enum TcpSocketType { - Stream, - Raw, -} diff --git a/nex-socket/src/tcp/sync_impl.rs b/nex-socket/src/tcp/sync_impl.rs index eaec8af..7a89263 100644 --- a/nex-socket/src/tcp/sync_impl.rs +++ b/nex-socket/src/tcp/sync_impl.rs @@ -24,7 +24,7 @@ impl TcpSocket { socket.set_nonblocking(config.nonblocking)?; - // Apply all configuration options + // Set socket options based on configuration if let Some(flag) = config.reuseaddr { socket.set_reuse_address(flag)?; } @@ -50,12 +50,13 @@ impl TcpSocket { socket.set_write_timeout(Some(timeout))?; } + // Linux: optional interface name #[cfg(any(target_os = "linux", target_os = "android", target_os = "fuchsia"))] if let Some(iface) = &config.bind_device { socket.bind_device(Some(iface.as_bytes()))?; } - // Bind to the specified address if provided + // bind to the specified address if provided if let Some(addr) = config.bind_addr { socket.bind(&addr.into())?; } @@ -90,16 +91,17 @@ impl TcpSocket { Self::new(Domain::IPV6, SockType::RAW) } - // --- socket operations --- - + /// Bind the socket to a specific address. pub fn bind(&self, addr: SocketAddr) -> io::Result<()> { self.socket.bind(&addr.into()) } + /// Connect to a remote address. pub fn connect(&self, addr: SocketAddr) -> io::Result<()> { self.socket.connect(&addr.into()) } + /// Connect to the target address with a timeout. #[cfg(unix)] pub fn connect_timeout(&self, target: SocketAddr, timeout: Duration) -> io::Result { let raw_fd = self.socket.as_raw_fd(); @@ -209,19 +211,23 @@ impl TcpSocket { Ok(std_stream) } + /// Start listening for incoming connections. pub fn listen(&self, backlog: i32) -> io::Result<()> { self.socket.listen(backlog) } + /// Accept an incoming connection. pub fn accept(&self) -> io::Result<(TcpStream, SocketAddr)> { let (stream, addr) = self.socket.accept()?; Ok((stream.into(), addr.as_socket().unwrap())) } + /// Convert the socket into a `TcpStream`. pub fn to_tcp_stream(self) -> io::Result { Ok(self.socket.into()) } + /// Convert the socket into a `TcpListener`. pub fn to_tcp_listener(self) -> io::Result { Ok(self.socket.into()) } @@ -249,36 +255,42 @@ impl TcpSocket { Ok((n, addr)) } + /// Shutdown the socket. pub fn shutdown(&self, how: std::net::Shutdown) -> io::Result<()> { self.socket.shutdown(how) } - // --- option helpers --- - + /// Set the socket to reuse the address. pub fn set_reuseaddr(&self, on: bool) -> io::Result<()> { self.socket.set_reuse_address(on) } + /// Set the socket to not delay packets. pub fn set_nodelay(&self, on: bool) -> io::Result<()> { self.socket.set_nodelay(on) } + /// Set the linger option for the socket. pub fn set_linger(&self, dur: Option) -> io::Result<()> { self.socket.set_linger(dur) } + /// Set the time-to-live for IPv4 packets. pub fn set_ttl(&self, ttl: u32) -> io::Result<()> { self.socket.set_ttl(ttl) } + /// Set the hop limit for IPv6 packets. pub fn set_hoplimit(&self, hops: u32) -> io::Result<()> { self.socket.set_unicast_hops_v6(hops) } + /// Set the keepalive option for the socket. pub fn set_keepalive(&self, on: bool) -> io::Result<()> { self.socket.set_keepalive(on) } + /// Set the bind device for the socket (Linux specific). pub fn set_bind_device(&self, iface: &str) -> io::Result<()> { #[cfg(any(target_os = "linux", target_os = "android", target_os = "fuchsia"))] return self.socket.bind_device(Some(iface.as_bytes())); @@ -293,6 +305,7 @@ impl TcpSocket { } } + /// Retrieve the local address of the socket. pub fn local_addr(&self) -> io::Result { self.socket .local_addr()? @@ -300,12 +313,14 @@ impl TcpSocket { .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Failed to retrieve local address")) } + /// Extract the RAW file descriptor for Unix. #[cfg(unix)] pub fn as_raw_fd(&self) -> std::os::unix::io::RawFd { use std::os::fd::AsRawFd; self.socket.as_raw_fd() } + /// Extract the RAW socket handle for Windows. #[cfg(windows)] pub fn as_raw_socket(&self) -> std::os::windows::io::RawSocket { use std::os::windows::io::AsRawSocket; diff --git a/nex-socket/src/udp/async_impl.rs b/nex-socket/src/udp/async_impl.rs index 15631fc..f9e9c51 100644 --- a/nex-socket/src/udp/async_impl.rs +++ b/nex-socket/src/udp/async_impl.rs @@ -17,6 +17,7 @@ impl AsyncUdpSocket { socket.set_nonblocking(true)?; + // Set socket options based on configuration if let Some(flag) = config.reuseaddr { socket.set_reuse_address(flag)?; } @@ -36,11 +37,13 @@ impl AsyncUdpSocket { socket.set_write_timeout(Some(timeout))?; } + // Linux: optional interface name #[cfg(any(target_os = "linux", target_os = "android", target_os = "fuchsia"))] if let Some(iface) = &config.bind_device { socket.bind_device(Some(iface.as_bytes()))?; } + // bind to the specified address if provided if let Some(addr) = config.bind_addr { socket.bind(&addr.into())?; } diff --git a/nex-socket/src/udp/config.rs b/nex-socket/src/udp/config.rs index a558017..f1e4146 100644 --- a/nex-socket/src/udp/config.rs +++ b/nex-socket/src/udp/config.rs @@ -4,6 +4,7 @@ use socket2::Type as SockType; use crate::SocketFamily; +/// UDP socket type, either DGRAM or RAW. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum UdpSocketType { Dgram, @@ -11,14 +12,17 @@ pub enum UdpSocketType { } impl UdpSocketType { + /// Returns true if the socket type is DGRAM. pub fn is_dgram(&self) -> bool { matches!(self, UdpSocketType::Dgram) } + /// Returns true if the socket type is RAW. pub fn is_raw(&self) -> bool { matches!(self, UdpSocketType::Raw) } + /// Converts the UDP socket type to a `socket2::Type`. pub(crate) fn to_sock_type(&self) -> SockType { match self { UdpSocketType::Dgram => SockType::DGRAM, @@ -36,22 +40,18 @@ pub struct UdpConfig { pub socket_type: UdpSocketType, /// Address to bind. If `None`, the operating system chooses the address. pub bind_addr: Option, - /// Enable address reuse (`SO_REUSEADDR`). pub reuseaddr: Option, - /// Allow broadcast (`SO_BROADCAST`). pub broadcast: Option, - /// Time to live value. pub ttl: Option, - /// Hop limit value. pub hoplimit: Option, - + /// Read timeout for the socket. pub read_timeout: Option, + /// Write timeout for the socket. pub write_timeout: Option, - /// Bind to a specific interface (Linux only). pub bind_device: Option, } diff --git a/nex-socket/src/udp/sync_impl.rs b/nex-socket/src/udp/sync_impl.rs index c687ee6..911b5c2 100644 --- a/nex-socket/src/udp/sync_impl.rs +++ b/nex-socket/src/udp/sync_impl.rs @@ -16,6 +16,7 @@ impl UdpSocket { socket.set_nonblocking(false)?; + // Set socket options based on configuration if let Some(flag) = config.reuseaddr { socket.set_reuse_address(flag)?; } @@ -35,11 +36,13 @@ impl UdpSocket { socket.set_write_timeout(Some(timeout))?; } + // Linux: optional interface name #[cfg(any(target_os = "linux", target_os = "android", target_os = "fuchsia"))] if let Some(iface) = &config.bind_device { socket.bind_device(Some(iface.as_bytes()))?; } + // bind to the specified address if provided if let Some(addr) = config.bind_addr { socket.bind(&addr.into())?; }