use std::net::Ipv4Addr;
use crate::MacAddr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct ArpMessage {
pub oper: ArpOp,
pub sender: MacAddr,
pub sender_ip: Ipv4Addr,
pub target: MacAddr,
pub target_ip: Ipv4Addr,
}
impl ArpMessage {
#[inline]
pub fn is_gratuitous(&self) -> bool {
self.sender_ip == self.target_ip
}
#[inline]
pub fn is_likely_spoof(&self) -> bool {
matches!(self.oper, ArpOp::Reply)
&& self.is_gratuitous()
&& self.target != self.sender
&& !self.target.is_zero()
&& !self.target.is_broadcast()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "serde",
serde(tag = "kind", content = "value", rename_all = "snake_case")
)]
#[non_exhaustive]
pub enum ArpOp {
Request,
Reply,
RarpRequest,
RarpReply,
Other(u16),
}
impl ArpOp {
pub fn as_str(&self) -> &'static str {
match self {
ArpOp::Request => "request",
ArpOp::Reply => "reply",
ArpOp::RarpRequest => "rarp_request",
ArpOp::RarpReply => "rarp_reply",
ArpOp::Other(_) => "other",
}
}
}
impl From<u16> for ArpOp {
fn from(op: u16) -> Self {
match op {
1 => ArpOp::Request,
2 => ArpOp::Reply,
3 => ArpOp::RarpRequest,
4 => ArpOp::RarpReply,
other => ArpOp::Other(other),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make(
oper: ArpOp,
sender: [u8; 6],
target: [u8; 6],
sip: [u8; 4],
tip: [u8; 4],
) -> ArpMessage {
ArpMessage {
oper,
sender: MacAddr(sender),
sender_ip: Ipv4Addr::new(sip[0], sip[1], sip[2], sip[3]),
target: MacAddr(target),
target_ip: Ipv4Addr::new(tip[0], tip[1], tip[2], tip[3]),
}
}
#[test]
fn arp_op_from_u16() {
assert!(matches!(ArpOp::from(1), ArpOp::Request));
assert!(matches!(ArpOp::from(2), ArpOp::Reply));
assert!(matches!(ArpOp::from(3), ArpOp::RarpRequest));
assert!(matches!(ArpOp::from(4), ArpOp::RarpReply));
assert!(matches!(ArpOp::from(99), ArpOp::Other(99)));
}
#[test]
fn arp_op_slugs() {
assert_eq!(ArpOp::Request.as_str(), "request");
assert_eq!(ArpOp::Reply.as_str(), "reply");
assert_eq!(ArpOp::RarpRequest.as_str(), "rarp_request");
assert_eq!(ArpOp::RarpReply.as_str(), "rarp_reply");
assert_eq!(ArpOp::Other(99).as_str(), "other");
}
#[test]
fn gratuitous_detection() {
let msg = make(
ArpOp::Request,
[1; 6],
[0; 6],
[10, 0, 0, 1],
[10, 0, 0, 1], );
assert!(msg.is_gratuitous());
let msg = make(ArpOp::Request, [1; 6], [0; 6], [10, 0, 0, 1], [10, 0, 0, 2]);
assert!(!msg.is_gratuitous());
}
#[test]
fn likely_spoof_detection() {
let msg = make(
ArpOp::Reply,
[1; 6],
[1; 6], [10, 0, 0, 1],
[10, 0, 0, 1],
);
assert!(msg.is_gratuitous());
assert!(!msg.is_likely_spoof());
let msg = make(
ArpOp::Reply,
[1; 6],
[2; 6], [10, 0, 0, 1],
[10, 0, 0, 1],
);
assert!(msg.is_likely_spoof());
let msg = make(ArpOp::Request, [1; 6], [2; 6], [10, 0, 0, 1], [10, 0, 0, 1]);
assert!(!msg.is_likely_spoof());
}
#[test]
fn broadcast_target_is_not_spoof() {
let msg = make(
ArpOp::Reply,
[1; 6],
[0xff; 6], [10, 0, 0, 1],
[10, 0, 0, 1],
);
assert!(msg.is_gratuitous());
assert!(!msg.is_likely_spoof());
}
}