Repository: GyulyVGC/sniffnet
Pull request #759: Add ARP support
Author: login: shu-kitamura
Labels: name: enhancement
description: New feature, request, or improvement
Created: 2025-03-16T02:53:04Z | Merged: 2025-03-22T23:39:14Z

Hello!
I'm shu-kitamura.

I took on the challenge #680.

ARP is listed in the protocol list in the menu.

<img width="944" alt="image" src="https://github.com/user-attachments/assets/b388b518-2109-41fa-ab38-18f529011407" />

ARP is also shown in the Inspect table.

![image](https://github.com/user-attachments/assets/f122425a-a98a-4615-97e8-32b3518e00a2)

When I click on the ARP row in the Inspect page table, the type of ARP operation is displayed.

<img width="941" alt="image" src="https://github.com/user-attachments/assets/3abca932-ceaa-4927-bebb-99f2e5585895" />


Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1475a6302..679edcd19 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
 All Sniffnet releases with the relative changes are documented in this file.
 
 ## [UNRELEASED]
+- Added support for ARP protocol ([#759](https://github.com/GyulyVGC/sniffnet/pull/759) — fixes [#680](https://github.com/GyulyVGC/sniffnet/issues/680))
 - Identify and tag unassigned/reserved "bogon" IP addresses ([#678](https://github.com/GyulyVGC/sniffnet/pull/678) — fixes [#209](https://github.com/GyulyVGC/sniffnet/issues/209))
 - Show data agglomerates in _Inspect_ page table ([#684](https://github.com/GyulyVGC/sniffnet/pull/684) — fixes [#601](https://github.com/GyulyVGC/sniffnet/issues/601))
 - Updated some of the existing translations to v1.3: 
diff --git a/src/gui/pages/connection_details_page.rs b/src/gui/pages/connection_details_page.rs
index 840011dda..834e7ace8 100644
--- a/src/gui/pages/connection_details_page.rs
+++ b/src/gui/pages/connection_details_page.rs
@@ -19,6 +19,7 @@ use crate::networking::manage_packets::{
     get_address_to_lookup, get_traffic_type, is_local_connection, is_my_address,
 };
 use crate::networking::types::address_port_pair::AddressPortPair;
+use crate::networking::types::arp_type::ArpType;
 use crate::networking::types::bogon::is_bogon;
 use crate::networking::types::host::Host;
 use crate::networking::types::icmp_type::IcmpType;
@@ -185,6 +186,7 @@ fn col_info<'a>(
     language: Language,
 ) -> Column<'a, Message, StyleType> {
     let is_icmp = key.protocol.eq(&Protocol::ICMP);
+    let is_arp = key.protocol.eq(&Protocol::ARP);
 
     let mut ret_val = Column::new()
         .spacing(10)
@@ -207,7 +209,7 @@ fn col_info<'a>(
             font,
         ));
 
-    if !is_icmp {
+    if !is_icmp && !is_arp {
         ret_val = ret_val.push(TextType::highlighted_subtitle_with_desc(
             service_translation(language),
             &val.service.to_string(),
@@ -234,7 +236,7 @@ fn col_info<'a>(
         font,
     ));
 
-    if is_icmp {
+    if is_icmp || is_arp {
         ret_val = ret_val.push(
             Column::new()
                 .push(
@@ -245,7 +247,14 @@ fn col_info<'a>(
                 .push(Scrollable::with_direction(
                     Column::new()
                         .padding(Padding::ZERO.right(10).bottom(10))
-                        .push(Text::new(IcmpType::pretty_print_types(&val.icmp_types)).font(font)),
+                        .push(
+                            Text::new(if is_icmp {
+                                IcmpType::pretty_print_types(&val.icmp_types)
+                            } else {
+                                ArpType::pretty_print_types(&val.arp_types)
+                            })
+                            .font(font),
+                        ),
                     Direction::Both {
                         vertical: ScrollbarType::properties(),
                         horizontal: ScrollbarType::properties(),
diff --git a/src/gui/pages/initial_page.rs b/src/gui/pages/initial_page.rs
index 205156978..6ab574985 100644
--- a/src/gui/pages/initial_page.rs
+++ b/src/gui/pages/initial_page.rs
@@ -131,7 +131,7 @@ fn col_ip_buttons(
                     .align_y(Alignment::Center)
                     .font(font),
             )
-            .width(90)
+            .width(80)
             .height(35)
             .class(if is_active {
                 ButtonType::BorderedRoundSelected
@@ -170,7 +170,7 @@ fn col_protocol_buttons(
                     .align_y(Alignment::Center)
                     .font(font),
             )
-            .width(90)
+            .width(80)
             .height(35)
             .class(if is_active {
                 ButtonType::BorderedRoundSelected
diff --git a/src/gui/sniffer.rs b/src/gui/sniffer.rs
index 62d41b273..3b0a9dafa 100644
--- a/src/gui/sniffer.rs
+++ b/src/gui/sniffer.rs
@@ -1056,11 +1056,16 @@ mod tests {
         sniffer.update(Message::ProtocolSelection(Protocol::UDP, false));
         assert_eq!(
             sniffer.filters.protocols,
-            HashSet::from([Protocol::TCP, Protocol::ICMP])
+            HashSet::from([Protocol::TCP, Protocol::ICMP, Protocol::ARP])
         );
         sniffer.update(Message::ProtocolSelection(Protocol::TCP, false));
-        assert_eq!(sniffer.filters.protocols, HashSet::from([Protocol::ICMP]));
+        assert_eq!(
+            sniffer.filters.protocols,
+            HashSet::from([Protocol::ICMP, Protocol::ARP])
+        );
         sniffer.update(Message::ProtocolSelection(Protocol::ICMP, false));
+        assert_eq!(sniffer.filters.protocols, HashSet::from([Protocol::ARP]));
+        sniffer.update(Message::ProtocolSelection(Protocol::ARP, false));
         assert_eq!(sniffer.filters.protocols, HashSet::new());
         sniffer.update(Message::ProtocolSelection(Protocol::UDP, true));
         assert_eq!(sniffer.filters.protocols, HashSet::from([Protocol::UDP]));
diff --git a/src/networking/manage_packets.rs b/src/networking/manage_packets.rs
index 7f2a35f45..ab3399809 100644
--- a/src/networking/manage_packets.rs
+++ b/src/networking/manage_packets.rs
@@ -5,13 +5,14 @@ use std::sync::Mutex;
 
 use chrono::Local;
 use dns_lookup::lookup_addr;
-use etherparse::{LaxPacketHeaders, LinkHeader, NetHeaders, TransportHeader};
+use etherparse::{EtherType, LaxPacketHeaders, LinkHeader, NetHeaders, TransportHeader};
 use pcap::{Address, Device};
 
 use crate::mmdb::asn::get_asn;
 use crate::mmdb::country::get_country;
 use crate::mmdb::types::mmdb_reader::MmdbReaders;
 use crate::networking::types::address_port_pair::AddressPortPair;
+use crate::networking::types::arp_type::ArpType;
 use crate::networking::types::bogon::is_bogon;
 use crate::networking::types::data_info_host::DataInfoHost;
 use crate::networking::types::host::Host;
@@ -37,6 +38,7 @@ pub fn analyze_headers(
     mac_addresses: &mut (Option<String>, Option<String>),
     exchanged_bytes: &mut u128,
     icmp_type: &mut IcmpType,
+    arp_type: &mut ArpType,
     packet_filters_fields: &mut PacketFiltersFields,
 ) -> Option<AddressPortPair> {
     analyze_link_header(
@@ -46,23 +48,28 @@ pub fn analyze_headers(
         exchanged_bytes,
     );
 
+    let is_arp = matches!(&headers.net, Some(NetHeaders::Arp(_)));
+
     if !analyze_network_header(
         headers.net,
         exchanged_bytes,
         &mut packet_filters_fields.ip_version,
         &mut packet_filters_fields.source,
         &mut packet_filters_fields.dest,
+        arp_type,
     ) {
         return None;
     }
 
-    if !analyze_transport_header(
-        headers.transport,
-        &mut packet_filters_fields.sport,
-        &mut packet_filters_fields.dport,
-        &mut packet_filters_fields.protocol,
-        icmp_type,
-    ) {
+    if !is_arp
+        && !analyze_transport_header(
+            headers.transport,
+            &mut packet_filters_fields.sport,
+            &mut packet_filters_fields.dport,
+            &mut packet_filters_fields.protocol,
+            icmp_type,
+        )
+    {
         return None;
     }
 
@@ -103,6 +110,7 @@ fn analyze_network_header(
     network_protocol: &mut IpVersion,
     address1: &mut IpAddr,
     address2: &mut IpAddr,
+    arp_type: &mut ArpType,
 ) -> bool {
     match network_header {
         Some(NetHeaders::Ipv4(ipv4header, _)) => {
@@ -119,6 +127,40 @@ fn analyze_network_header(
             *exchanged_bytes += u128::from(40 + ipv6header.payload_length);
             true
         }
+        Some(NetHeaders::Arp(arp_packet)) => {
+            match arp_packet.proto_addr_type {
+                EtherType::IPV4 => {
+                    *network_protocol = IpVersion::IPv4;
+                    *address1 =
+                        match TryInto::<[u8; 4]>::try_into(arp_packet.sender_protocol_addr()) {
+                            Ok(source) => IpAddr::from(source),
+                            Err(_) => return false,
+                        };
+                    *address2 =
+                        match TryInto::<[u8; 4]>::try_into(arp_packet.target_protocol_addr()) {
+                            Ok(destination) => IpAddr::from(destination),
+                            Err(_) => return false,
+                        };
+                }
+                EtherType::IPV6 => {
+                    *network_protocol = IpVersion::IPv6;
+                    *address1 =
+                        match TryInto::<[u8; 16]>::try_into(arp_packet.sender_protocol_addr()) {
+                            Ok(source) => IpAddr::from(source),
+                            Err(_) => return false,
+                        };
+                    *address2 =
+                        match TryInto::<[u8; 16]>::try_into(arp_packet.target_protocol_addr()) {
+                            Ok(destination) => IpAddr::from(destination),
+                            Err(_) => return false,
+                        };
+                }
+                _ => return false,
+            }
+            *exchanged_bytes += arp_packet.packet_len() as u128;
+            *arp_type = ArpType::from_etherparse(arp_packet.operation);
+            true
+        }
         _ => false,
     }
 }
@@ -165,7 +207,11 @@ fn analyze_transport_header(
 }
 
 pub fn get_service(key: &AddressPortPair, traffic_direction: TrafficDirection) -> Service {
-    if key.port1.is_none() || key.port2.is_none() || key.protocol == Protocol::ICMP {
+    if key.port1.is_none()
+        || key.port2.is_none()
+        || key.protocol == Protocol::ICMP
+        || key.protocol == Protocol::ARP
+    {
         return Service::NotApplicable;
     }
 
@@ -217,6 +263,7 @@ pub fn modify_or_insert_in_map(
     my_device: &MyDevice,
     mac_addresses: (Option<String>, Option<String>),
     icmp_type: IcmpType,
+    arp_type: ArpType,
     exchanged_bytes: u128,
 ) -> InfoAddressPortPair {
     let now = Local::now();
@@ -268,6 +315,12 @@ pub fn modify_or_insert_in_map(
                     .and_modify(|n| *n += 1)
                     .or_insert(1);
             }
+            if key.protocol.eq(&Protocol::ARP) {
+                info.arp_types
+                    .entry(arp_type)
+                    .and_modify(|n| *n += 1)
+                    .or_insert(1);
+            }
         })
         .or_insert_with(|| InfoAddressPortPair {
             mac_address1: mac_addresses.0,
@@ -283,6 +336,11 @@ pub fn modify_or_insert_in_map(
             } else {
                 HashMap::new()
             },
+            arp_types: if key.protocol.eq(&Protocol::ARP) {
+                HashMap::from([(arp_type, 1)])
+            } else {
+                HashMap::new()
+            },
         })
         .clone();
 
@@ -1239,7 +1297,7 @@ mod tests {
                     Service::Name(match p {
                         Protocol::TCP => "mdns",
                         Protocol::UDP => "zeroconf",
-                        Protocol::ICMP => panic!(),
+                        _ => panic!(),
                     })
                 );
 
@@ -1249,7 +1307,7 @@ mod tests {
                     match p {
                         Protocol::TCP => Service::Name("netstat"),
                         Protocol::UDP => Service::Unknown,
-                        Protocol::ICMP => panic!(),
+                        _ => panic!(),
                     }
                 );
 
@@ -1260,7 +1318,7 @@ mod tests {
                     match p {
                         Protocol::TCP => Service::Unknown,
                         Protocol::UDP => Service::Name("murmur"),
-                        Protocol::ICMP => panic!(),
+                        _ => panic!(),
                     }
                 );
 
diff --git a/src/networking/types/arp_type.rs b/src/networking/types/arp_type.rs
new file mode 100644
index 000000000..cb8a158c1
--- /dev/null
+++ b/src/networking/types/arp_type.rs
@@ -0,0 +1,48 @@
+use std::collections::HashMap;
+use std::fmt::{Display, Formatter};
+
+use etherparse::ArpOperation;
+
+#[derive(Copy, Clone, PartialEq, Eq, Hash, Default)]
+pub enum ArpType {
+    Request,
+    Reply,
+    #[default]
+    Unknown,
+}
+
+impl ArpType {
+    pub fn from_etherparse(arp_operation: ArpOperation) -> ArpType {
+        match arp_operation {
+            ArpOperation(1) => Self::Request,
+            ArpOperation(2) => Self::Reply,
+            ArpOperation(_) => Self::Unknown,
+        }
+    }
+
+    pub fn pretty_print_types(map: &HashMap<ArpType, usize>) -> String {
+        let mut ret_val = String::new();
+
+        let mut vec: Vec<(&ArpType, &usize)> = map.iter().collect();
+        vec.sort_by(|(_, a), (_, b)| b.cmp(a));
+
+        for (arp_type, n) in vec {
+            ret_val.push_str(&format!("   {arp_type} ({n})\n"));
+        }
+        ret_val
+    }
+}
+
+impl Display for ArpType {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "{}",
+            match self {
+                ArpType::Request => "Request",
+                ArpType::Reply => "Reply",
+                ArpType::Unknown => "?",
+            }
+        )
+    }
+}
diff --git a/src/networking/types/info_address_port_pair.rs b/src/networking/types/info_address_port_pair.rs
index 44e8febfd..52b39ce80 100644
--- a/src/networking/types/info_address_port_pair.rs
+++ b/src/networking/types/info_address_port_pair.rs
@@ -5,6 +5,7 @@ use std::collections::HashMap;
 
 use chrono::{DateTime, Local};
 
+use crate::networking::types::arp_type::ArpType;
 use crate::networking::types::icmp_type::IcmpType;
 use crate::networking::types::traffic_direction::TrafficDirection;
 use crate::Service;
@@ -32,4 +33,6 @@ pub struct InfoAddressPortPair {
     pub traffic_direction: TrafficDirection,
     /// Types of the ICMP messages exchanged, with the relative count (this is empty if not ICMP)
     pub icmp_types: HashMap<IcmpType, usize>,
+    /// Types of the ARP operations, with the relative count (this is empty if not ARP)
+    pub arp_types: HashMap<ArpType, usize>,
 }
diff --git a/src/networking/types/mod.rs b/src/networking/types/mod.rs
index 93c87318a..8553faafb 100644
--- a/src/networking/types/mod.rs
+++ b/src/networking/types/mod.rs
@@ -1,4 +1,5 @@
 pub mod address_port_pair;
+pub mod arp_type;
 pub mod asn;
 pub mod bogon;
 pub mod byte_multiple;
diff --git a/src/networking/types/packet_filters_fields.rs b/src/networking/types/packet_filters_fields.rs
index 268989f9e..2767160c8 100644
--- a/src/networking/types/packet_filters_fields.rs
+++ b/src/networking/types/packet_filters_fields.rs
@@ -24,7 +24,7 @@ impl Default for PacketFiltersFields {
     fn default() -> Self {
         Self {
             ip_version: IpVersion::IPv4,
-            protocol: Protocol::TCP,
+            protocol: Protocol::ARP,
             source: IpAddr::from_str("::").unwrap(),
             dest: IpAddr::from_str("::").unwrap(),
             sport: None,
diff --git a/src/networking/types/protocol.rs b/src/networking/types/protocol.rs
index 40ac658e8..31f20e625 100644
--- a/src/networking/types/protocol.rs
+++ b/src/networking/types/protocol.rs
@@ -10,6 +10,8 @@ pub enum Protocol {
     UDP,
     /// Internet Control Message Protocol
     ICMP,
+    /// Address Resolution Protocol
+    ARP,
 }
 
 impl std::fmt::Display for Protocol {
@@ -19,7 +21,7 @@ impl std::fmt::Display for Protocol {
 }
 
 impl Protocol {
-    pub const ALL: [Protocol; 3] = [Protocol::TCP, Protocol::UDP, Protocol::ICMP];
+    pub const ALL: [Protocol; 4] = [Protocol::TCP, Protocol::UDP, Protocol::ICMP, Protocol::ARP];
 }
 
 #[cfg(test)]
@@ -33,15 +35,17 @@ mod tests {
                 Protocol::TCP => assert_eq!(protocol.to_string(), "TCP"),
                 Protocol::UDP => assert_eq!(protocol.to_string(), "UDP"),
                 Protocol::ICMP => assert_eq!(protocol.to_string(), "ICMP"),
+                Protocol::ARP => assert_eq!(protocol.to_string(), "ARP"),
             }
         }
     }
 
     #[test]
     fn test_all_protocols_collection() {
-        assert_eq!(Protocol::ALL.len(), 3);
+        assert_eq!(Protocol::ALL.len(), 4);
         assert_eq!(Protocol::ALL.get(0).unwrap(), &Protocol::TCP);
         assert_eq!(Protocol::ALL.get(1).unwrap(), &Protocol::UDP);
         assert_eq!(Protocol::ALL.get(2).unwrap(), &Protocol::ICMP);
+        assert_eq!(Protocol::ALL.get(3).unwrap(), &Protocol::ARP);
     }
 }
diff --git a/src/secondary_threads/parse_packets.rs b/src/secondary_threads/parse_packets.rs
index 41a215ca7..e695e39df 100644
--- a/src/secondary_threads/parse_packets.rs
+++ b/src/secondary_threads/parse_packets.rs
@@ -13,6 +13,7 @@ use crate::mmdb::types::mmdb_reader::MmdbReaders;
 use crate::networking::manage_packets::{
     analyze_headers, get_address_to_lookup, modify_or_insert_in_map, reverse_dns_lookup,
 };
+use crate::networking::types::arp_type::ArpType;
 use crate::networking::types::capture_context::CaptureContext;
 use crate::networking::types::data_info::DataInfo;
 use crate::networking::types::filters::Filters;
@@ -56,6 +57,7 @@ pub fn parse_packets(
                     let mut exchanged_bytes = 0;
                     let mut mac_addresses = (None, None);
                     let mut icmp_type = IcmpType::default();
+                    let mut arp_type = ArpType::default();
                     let mut packet_filters_fields = PacketFiltersFields::default();
 
                     let key_option = analyze_headers(
@@ -63,8 +65,10 @@ pub fn parse_packets(
                         &mut mac_addresses,
                         &mut exchanged_bytes,
                         &mut icmp_type,
+                        &mut arp_type,
                         &mut packet_filters_fields,
                     );
+
                     if key_option.is_none() {
                         continue;
                     }
@@ -85,6 +89,7 @@ pub fn parse_packets(
                             device,
                             mac_addresses,
                             icmp_type,
+                            arp_type,
                             exchanged_bytes,
                         );
                     }


Reviews:
user: login: GyulyVGC
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-16T10:21:06Z
user: login: shu-kitamura
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-16T13:46:44Z
user: login: GyulyVGC
body: :ship:
state: APPROVED
author_association: OWNER
submitted_at: 2025-03-22T23:35:20Z

Review comments:
diff_hunk: @@ -119,6 +130,20 @@ fn analyze_network_header(
             *exchanged_bytes += u128::from(40 + ipv6header.payload_length);
             true
         }
+        Some(NetHeaders::Arp(arp_packet)) => {
+            *network_protocol = match arp_packet.proto_addr_type {
+                EtherType::IPV4 => IpVersion::IPv4,
+                EtherType::IPV6 => IpVersion::IPv6,
+                _ => return false,
+            };
+            let src: [u8; 4] = arp_packet.sender_protocol_addr().try_into().unwrap();
path: src/networking/manage_packets.rs
user: login: GyulyVGC
body: From a quick look I think this is going to panic in presence of IPv6 ARP. If the protocol addresses aren't of the expected length OR they don't are IPv4/IPv6 addresses, we want to ignore that packet.
created_at: 2025-03-16T10:21:06Z
author_association: OWNER
diff_hunk: @@ -119,6 +130,20 @@ fn analyze_network_header(
             *exchanged_bytes += u128::from(40 + ipv6header.payload_length);
             true
         }
+        Some(NetHeaders::Arp(arp_packet)) => {
+            *network_protocol = match arp_packet.proto_addr_type {
+                EtherType::IPV4 => IpVersion::IPv4,
+                EtherType::IPV6 => IpVersion::IPv6,
+                _ => return false,
+            };
+            let src: [u8; 4] = arp_packet.sender_protocol_addr().try_into().unwrap();
path: src/networking/manage_packets.rs
user: login: shu-kitamura
body: Thanks for your quick check.

About IPv6, I overlooked...

In commit https://github.com/GyulyVGC/sniffnet/pull/759/commits/35d42d0d531af3f33e735b91c617795d8d6d666d, I fixed to not panic when the address is IPv6.
Also, if the protocol address is not the expected length or is not an IPv4/IPv6 address, the packet will be ignored.
created_at: 2025-03-16T13:46:44Z
author_association: CONTRIBUTOR

PR comments:
user: login: GyulyVGC
created_at: 2025-03-16T07:08:34Z
body: Thank you so much!

I'm pretty busy these days, but I'll review the PR as soon as I get some time. 🙌
author_association: OWNER
user: login: GyulyVGC
created_at: 2025-03-22T23:36:03Z
body: @all-contributors please add @shu-kitamura for code.
author_association: OWNER
user: login: allcontributors[bot]
created_at: 2025-03-22T23:36:13Z
body: @GyulyVGC 

I've put up [a pull request](https://github.com/GyulyVGC/sniffnet/pull/762) to add @shu-kitamura! :tada:
author_association: CONTRIBUTOR