Skip to main content

nex_socket/icmp/
config.rs

1use socket2::Type as SockType;
2use std::{io, net::SocketAddr, time::Duration};
3
4use crate::SocketFamily;
5
6/// ICMP protocol version.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8#[non_exhaustive]
9pub enum IcmpKind {
10    V4,
11    V6,
12}
13
14/// ICMP socket type, either DGRAM or RAW.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16#[non_exhaustive]
17pub enum IcmpSocketType {
18    Dgram,
19    Raw,
20}
21
22impl IcmpSocketType {
23    /// Returns true if the socket type is DGRAM.
24    pub fn is_dgram(&self) -> bool {
25        matches!(self, IcmpSocketType::Dgram)
26    }
27
28    /// Returns true if the socket type is RAW.
29    pub fn is_raw(&self) -> bool {
30        matches!(self, IcmpSocketType::Raw)
31    }
32
33    /// Converts the ICMP socket type from a `socket2::Type`.
34    pub(crate) fn try_from_sock_type(sock_type: SockType) -> io::Result<Self> {
35        match sock_type {
36            SockType::DGRAM => Ok(IcmpSocketType::Dgram),
37            SockType::RAW => Ok(IcmpSocketType::Raw),
38            _ => Err(io::Error::new(
39                io::ErrorKind::InvalidInput,
40                "invalid ICMP socket type",
41            )),
42        }
43    }
44
45    /// Converts the ICMP socket type to a `socket2::Type`.
46    pub(crate) fn to_sock_type(self) -> SockType {
47        match self {
48            IcmpSocketType::Dgram => SockType::DGRAM,
49            IcmpSocketType::Raw => SockType::RAW,
50        }
51    }
52}
53
54/// Configuration for an ICMP socket.
55#[derive(Debug, Clone)]
56#[non_exhaustive]
57pub struct IcmpConfig {
58    /// The socket family.
59    pub socket_family: SocketFamily,
60    /// Optional bind address for the socket.
61    pub bind: Option<SocketAddr>,
62    /// Time-to-live for IPv4 packets.
63    pub ttl: Option<u32>,
64    /// Hop limit for IPv6 packets.
65    pub hoplimit: Option<u32>,
66    /// Read timeout for the socket.
67    pub read_timeout: Option<Duration>,
68    /// Write timeout for the socket.
69    pub write_timeout: Option<Duration>,
70    /// Network interface to use for the socket.
71    pub interface: Option<String>,
72    /// Socket type hint, DGRAM preferred on Linux, RAW fallback on macOS/Windows.
73    pub sock_type_hint: IcmpSocketType,
74    /// FreeBSD only: optional FIB (Forwarding Information Base) support.
75    pub fib: Option<u32>,
76}
77
78impl IcmpConfig {
79    /// Creates a new ICMP configuration with the specified kind.
80    pub fn new(kind: IcmpKind) -> Self {
81        Self {
82            socket_family: match kind {
83                IcmpKind::V4 => SocketFamily::IPV4,
84                IcmpKind::V6 => SocketFamily::IPV6,
85            },
86            bind: None,
87            ttl: None,
88            hoplimit: None,
89            read_timeout: None,
90            write_timeout: None,
91            interface: None,
92            sock_type_hint: IcmpSocketType::Dgram,
93            fib: None,
94        }
95    }
96
97    /// Creates a new ICMP configuration from a socket family.
98    pub fn from_family(socket_family: SocketFamily) -> Self {
99        Self {
100            socket_family,
101            ..Self::new(match socket_family {
102                SocketFamily::IPV4 => IcmpKind::V4,
103                SocketFamily::IPV6 => IcmpKind::V6,
104            })
105        }
106    }
107
108    /// Set bind address for the socket.
109    pub fn with_bind(mut self, addr: SocketAddr) -> Self {
110        self.bind = Some(addr);
111        self
112    }
113
114    /// Set the time-to-live for IPv4 packets.
115    pub fn with_ttl(mut self, ttl: u32) -> Self {
116        self.ttl = Some(ttl);
117        self
118    }
119
120    /// Set the hop limit for IPv6 packets.
121    pub fn with_hoplimit(mut self, hops: u32) -> Self {
122        self.hoplimit = Some(hops);
123        self
124    }
125
126    /// Set the hop limit for IPv6 packets.
127    pub fn with_hop_limit(self, hops: u32) -> Self {
128        self.with_hoplimit(hops)
129    }
130
131    /// Set the read timeout for the socket.
132    pub fn with_read_timeout(mut self, timeout: Duration) -> Self {
133        self.read_timeout = Some(timeout);
134        self
135    }
136
137    /// Set the write timeout for the socket.
138    pub fn with_write_timeout(mut self, timeout: Duration) -> Self {
139        self.write_timeout = Some(timeout);
140        self
141    }
142
143    /// Set the network interface to use for the socket.
144    pub fn with_interface(mut self, iface: impl Into<String>) -> Self {
145        self.interface = Some(iface.into());
146        self
147    }
148
149    /// Set the socket type hint. (DGRAM or RAW)
150    pub fn with_sock_type(mut self, ty: IcmpSocketType) -> Self {
151        self.sock_type_hint = ty;
152        self
153    }
154
155    /// Set the FIB (Forwarding Information Base) for FreeBSD.
156    pub fn with_fib(mut self, fib: u32) -> Self {
157        self.fib = Some(fib);
158        self
159    }
160
161    /// Validate the configuration before socket creation.
162    pub fn validate(&self) -> io::Result<()> {
163        if let Some(addr) = self.bind {
164            let addr_family = crate::SocketFamily::from_socket_addr(&addr);
165            if addr_family != self.socket_family {
166                return Err(io::Error::new(
167                    io::ErrorKind::InvalidInput,
168                    "bind address family does not match socket_family",
169                ));
170            }
171        }
172
173        if self.socket_family.is_v4() && self.hoplimit.is_some() {
174            return Err(io::Error::new(
175                io::ErrorKind::InvalidInput,
176                "hoplimit is only supported for IPv6 ICMP sockets",
177            ));
178        }
179
180        if self.socket_family.is_v6() && self.ttl.is_some() {
181            return Err(io::Error::new(
182                io::ErrorKind::InvalidInput,
183                "ttl is only supported for IPv4 ICMP sockets",
184            ));
185        }
186
187        if matches!(self.read_timeout, Some(timeout) if timeout.is_zero()) {
188            return Err(io::Error::new(
189                io::ErrorKind::InvalidInput,
190                "read_timeout must be greater than zero",
191            ));
192        }
193
194        if matches!(self.write_timeout, Some(timeout) if timeout.is_zero()) {
195            return Err(io::Error::new(
196                io::ErrorKind::InvalidInput,
197                "write_timeout must be greater than zero",
198            ));
199        }
200
201        if matches!(self.interface.as_deref(), Some("")) {
202            return Err(io::Error::new(
203                io::ErrorKind::InvalidInput,
204                "interface must not be empty",
205            ));
206        }
207
208        Ok(())
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn icmp_config_builders() {
218        let addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
219        let cfg = IcmpConfig::new(IcmpKind::V4)
220            .with_bind(addr)
221            .with_ttl(4)
222            .with_interface("eth0")
223            .with_sock_type(IcmpSocketType::Raw);
224        assert_eq!(cfg.socket_family, SocketFamily::IPV4);
225        assert_eq!(cfg.bind, Some(addr));
226        assert_eq!(cfg.ttl, Some(4));
227        assert_eq!(cfg.interface.as_deref(), Some("eth0"));
228        assert_eq!(cfg.sock_type_hint, IcmpSocketType::Raw);
229    }
230
231    #[test]
232    fn from_family_sets_expected_kind() {
233        let v4 = IcmpConfig::from_family(SocketFamily::IPV4);
234        let v6 = IcmpConfig::from_family(SocketFamily::IPV6);
235        assert_eq!(v4.socket_family, SocketFamily::IPV4);
236        assert_eq!(v6.socket_family, SocketFamily::IPV6);
237    }
238
239    #[test]
240    fn icmp_config_validate_rejects_family_mismatch() {
241        let cfg = IcmpConfig::new(IcmpKind::V4).with_bind("[::1]:0".parse().unwrap());
242        assert!(cfg.validate().is_err());
243    }
244}