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