Overview
The Address Resolution Protocol (ARP) operates at the network interface layer to resolve Internet Protocol (IP) addresses into Media Access Control (MAC) addresses within local networks. When a device needs to communicate with another device on the same network segment, it knows the destination IP address but requires the corresponding MAC address to frame the data link layer packet. ARP provides the mechanism for this translation.
ARP functions through a request-response pattern. A device broadcasts an ARP request containing the target IP address to all devices on the local network segment. The device with the matching IP address responds with an ARP reply containing its MAC address. Both the requesting device and the responding device cache this address mapping to avoid repeated broadcasts for subsequent communications.
The protocol specification, defined in RFC 826, describes a hardware-independent method for address resolution that works across different network technologies. While most commonly associated with IPv4 and Ethernet networks, ARP supports multiple protocol and hardware address formats through its flexible packet structure.
# Example ARP cache inspection in Ruby using system commands
require 'open3'
def read_arp_cache
stdout, stderr, status = Open3.capture3('arp', '-a')
if status.success?
entries = []
stdout.each_line do |line|
if line =~ /\((\d+\.\d+\.\d+\.\d+)\) at ([0-9a-f:]+)/
entries << { ip: $1, mac: $2 }
end
end
entries
else
raise "Failed to read ARP cache: #{stderr}"
end
end
cache = read_arp_cache
cache.each { |entry| puts "#{entry[:ip]} -> #{entry[:mac]}" }
# => 192.168.1.1 -> aa:bb:cc:dd:ee:ff
# => 192.168.1.50 -> 11:22:33:44:55:66
ARP cache entries have limited lifetimes to accommodate network topology changes. Operating systems implement varying cache timeout policies, typically ranging from several minutes to hours for valid entries. This temporal aspect introduces complexity in network programming and diagnostics.
Key Principles
ARP operates on the principle that devices within the same broadcast domain can communicate directly at the data link layer once they know each other's hardware addresses. The protocol assumes a broadcast-capable medium where all devices can receive packets addressed to a broadcast address.
The ARP packet structure contains fields for hardware type, protocol type, hardware address length, protocol address length, operation code, and the sender and target hardware and protocol addresses. The hardware type field typically contains value 1 for Ethernet, while the protocol type field contains 0x0800 for IPv4. Operation codes distinguish between ARP requests (1) and ARP replies (2).
When a device needs to send a packet to an IP address on the local network, it first checks its ARP cache for an existing mapping. If no entry exists or the entry has expired, the device constructs an ARP request packet with the sender's IP and MAC addresses in the sender fields, the target IP address in the target protocol address field, and zeroes or a broadcast address in the target hardware address field. The device broadcasts this request to the MAC address ff:ff:ff:ff:ff:ff.
All devices on the network segment receive the broadcast ARP request. Each device compares the target IP address in the request with its own IP address. Devices with non-matching IP addresses discard the packet. The device with the matching IP address constructs an ARP reply containing its MAC address and sends it directly to the requester using the sender hardware and protocol addresses from the request.
Both the requesting and responding devices update their ARP caches with the learned mappings. The requester learns the target's MAC address from the reply, while the responder learns the requester's mapping from the request itself. This optimization reduces future ARP traffic by caching mappings proactively.
# Conceptual ARP packet structure in Ruby
class ARPPacket
HARDWARE_TYPE_ETHERNET = 1
PROTOCOL_TYPE_IPV4 = 0x0800
OPERATION_REQUEST = 1
OPERATION_REPLY = 2
attr_accessor :hardware_type, :protocol_type, :hw_addr_length,
:proto_addr_length, :operation, :sender_hw_addr,
:sender_proto_addr, :target_hw_addr, :target_proto_addr
def initialize
@hardware_type = HARDWARE_TYPE_ETHERNET
@protocol_type = PROTOCOL_TYPE_IPV4
@hw_addr_length = 6 # MAC address bytes
@proto_addr_length = 4 # IPv4 address bytes
end
def request?
operation == OPERATION_REQUEST
end
def reply?
operation == OPERATION_REPLY
end
def to_bytes
# Simplified serialization
[
hardware_type,
protocol_type,
hw_addr_length,
proto_addr_length,
operation
].pack('nnnCC') +
sender_hw_addr + sender_proto_addr +
target_hw_addr + target_proto_addr
end
end
ARP cache management follows specific rules for entry lifecycle. Dynamic entries result from ARP request-reply exchanges and expire after timeout periods. Static entries can be manually configured and persist until explicitly removed or system restart. Incomplete entries occur when an ARP request has been sent but no reply received, typically maintained for short durations before retry or discard.
The protocol includes no authentication or verification mechanism in its basic form. Any device can send ARP replies, and receiving devices accept these replies without validation. This design choice prioritizes simplicity and performance over security, reflecting the protocol's origins in trusted network environments.
Ruby Implementation
Ruby provides multiple approaches for working with ARP, ranging from system command execution to raw socket programming. The standard library includes socket support that enables low-level network operations, though ARP-specific functionality typically requires privileged access and careful packet construction.
The most direct approach uses system commands through backticks, system calls, or the Open3 module. This method relies on operating system utilities like arp, ip, or arping to interact with the ARP cache and send ARP requests. While not pure Ruby, this approach offers simplicity and cross-platform compatibility.
require 'open3'
class ARPCacheManager
def self.get_entry(ip_address)
stdout, stderr, status = Open3.capture3('arp', '-n', ip_address)
return nil unless status.success?
if stdout =~ /(\h{2}:\h{2}:\h{2}:\h{2}:\h{2}:\h{2})/
{ ip: ip_address, mac: $1, type: :dynamic }
end
end
def self.add_static_entry(ip_address, mac_address)
# Requires root/administrator privileges
stdout, stderr, status = Open3.capture3(
'arp', '-s', ip_address, mac_address
)
status.success?
end
def self.delete_entry(ip_address)
stdout, stderr, status = Open3.capture3('arp', '-d', ip_address)
status.success?
end
def self.all_entries
stdout, stderr, status = Open3.capture3('arp', '-a')
return [] unless status.success?
entries = []
stdout.each_line do |line|
next unless line =~ /\((\d+\.\d+\.\d+\.\d+)\) at ([0-9a-f:]+)/
entries << {
ip: $1,
mac: $2,
type: line.include?('PERM') ? :static : :dynamic
}
end
entries
end
end
# Usage
entry = ARPCacheManager.get_entry('192.168.1.1')
puts "Router MAC: #{entry[:mac]}" if entry
# => Router MAC: aa:bb:cc:dd:ee:ff
For more direct control, Ruby's socket library enables raw packet construction and transmission. Raw sockets require elevated privileges and knowledge of packet structure. The PacketFu gem provides higher-level abstractions for packet manipulation.
require 'socket'
class RawARPSender
def initialize(interface)
@interface = interface
@socket = Socket.new(Socket::AF_PACKET, Socket::SOCK_RAW, 0x0806)
# Get interface index
ifreq = [@interface].pack('a16')
@socket.ioctl(0x8933, ifreq) # SIOCGIFINDEX
@if_index = ifreq.unpack('a16i')[1]
end
def send_arp_request(target_ip, source_ip, source_mac)
packet = build_arp_request(target_ip, source_ip, source_mac)
# Socket address structure for AF_PACKET
sll = [
Socket::AF_PACKET,
0x0806, # ETH_P_ARP in network byte order
@if_index,
1, # PACKET_BROADCAST
6, # Hardware address length
'ff:ff:ff:ff:ff:ff'.split(':').map(&:hex).pack('C6')
].pack('SSnCCa8')
@socket.send(packet, 0, sll)
end
private
def build_arp_request(target_ip, source_ip, source_mac)
# Ethernet header
eth_dst = ['ff', 'ff', 'ff', 'ff', 'ff', 'ff'].map(&:hex).pack('C6')
eth_src = source_mac.split(':').map(&:hex).pack('C6')
eth_type = [0x0806].pack('n')
# ARP header
hw_type = [1].pack('n') # Ethernet
proto_type = [0x0800].pack('n') # IPv4
hw_size = [6].pack('C')
proto_size = [4].pack('C')
opcode = [1].pack('n') # Request
# ARP payload
sender_mac = source_mac.split(':').map(&:hex).pack('C6')
sender_ip = source_ip.split('.').map(&:to_i).pack('C4')
target_mac = [0, 0, 0, 0, 0, 0].pack('C6')
target_ip_bytes = target_ip.split('.').map(&:to_i).pack('C4')
eth_dst + eth_src + eth_type +
hw_type + proto_type + hw_size + proto_size + opcode +
sender_mac + sender_ip + target_mac + target_ip_bytes
end
end
The pcap library, accessible through the pcaprub gem, provides packet capture capabilities for monitoring ARP traffic. This enables analysis of ARP requests and replies on the network.
require 'pcaprub'
class ARPMonitor
def initialize(interface)
@interface = interface
@capture = PCAPRUB::Pcap.open_live(interface, 65535, true, 1000)
@capture.setfilter('arp')
end
def monitor(duration = 60)
packets = []
start_time = Time.now
while Time.now - start_time < duration
@capture.each do |packet_data|
packets << parse_arp_packet(packet_data)
end
end
packets
end
private
def parse_arp_packet(data)
# Skip Ethernet header (14 bytes)
arp_data = data[14..-1]
hw_type = arp_data[0..1].unpack1('n')
proto_type = arp_data[2..3].unpack1('n')
hw_size = arp_data[4].unpack1('C')
proto_size = arp_data[5].unpack1('C')
opcode = arp_data[6..7].unpack1('n')
sender_mac = arp_data[8..13].unpack('C6').map { |b| '%02x' % b }.join(':')
sender_ip = arp_data[14..17].unpack('C4').join('.')
target_mac = arp_data[18..23].unpack('C6').map { |b| '%02x' % b }.join(':')
target_ip = arp_data[24..27].unpack('C4').join('.')
{
type: opcode == 1 ? :request : :reply,
sender_mac: sender_mac,
sender_ip: sender_ip,
target_mac: target_mac,
target_ip: target_ip,
timestamp: Time.now
}
end
end
Practical Examples
Network diagnostic tools frequently query ARP caches to troubleshoot connectivity issues. A device unable to communicate with another device on the local network may lack an ARP entry or possess an incorrect mapping. Verifying ARP cache contents helps identify such problems.
require 'open3'
require 'ipaddr'
class NetworkDiagnostics
def self.diagnose_connectivity(target_ip)
results = {
ip: target_ip,
ping_success: false,
arp_entry: nil,
recommendations: []
}
# Check ARP cache
arp_output, = Open3.capture3('arp', '-n', target_ip)
if arp_output =~ /([0-9a-f:]{17})/
results[:arp_entry] = $1
else
results[:recommendations] << "No ARP entry found - host may be offline or unreachable"
end
# Test connectivity
ping_output, = Open3.capture3('ping', '-c', '1', '-W', '1', target_ip)
results[:ping_success] = ping_output.include?('1 received')
# Analyze results
if results[:ping_success] && results[:arp_entry]
results[:recommendations] << "Connectivity confirmed"
elsif !results[:ping_success] && results[:arp_entry]
results[:recommendations] << "ARP entry exists but ping fails - check firewall or routing"
elsif results[:ping_success] && !results[:arp_entry]
results[:recommendations] << "Ping succeeded - ARP entry may have expired, run diagnosis again"
else
results[:recommendations] << "Cannot reach host - verify IP address and network connectivity"
end
results
end
end
# Usage
diagnosis = NetworkDiagnostics.diagnose_connectivity('192.168.1.100')
puts "Target: #{diagnosis[:ip]}"
puts "ARP Entry: #{diagnosis[:arp_entry] || 'None'}"
puts "Ping: #{diagnosis[:ping_success] ? 'Success' : 'Failed'}"
diagnosis[:recommendations].each { |r| puts "- #{r}" }
Network mapping applications scan local networks to discover active hosts. This process sends ARP requests to all possible IP addresses in a subnet and records which addresses respond. The resulting map shows the network topology and active devices.
require 'open3'
require 'ipaddr'
require 'thread'
class NetworkScanner
def initialize(network_cidr)
@network = IPAddr.new(network_cidr)
@mutex = Mutex.new
@results = []
end
def scan(threads: 50)
queue = Queue.new
# Generate all host IPs in network
@network.to_range.each do |ip|
next if ip == @network.to_range.first # Skip network address
next if ip == @network.to_range.last # Skip broadcast address
queue << ip.to_s
end
# Create worker threads
workers = threads.times.map do
Thread.new do
while !queue.empty?
begin
ip = queue.pop(true)
check_host(ip)
rescue ThreadError
break # Queue empty
end
end
end
end
workers.each(&:join)
@results
end
private
def check_host(ip)
# Send single ping to trigger ARP request
stdout, = Open3.capture3('ping', '-c', '1', '-W', '1', ip)
if stdout.include?('1 received')
# Get ARP entry
arp_output, = Open3.capture3('arp', '-n', ip)
if arp_output =~ /([0-9a-f:]{17})/
@mutex.synchronize do
@results << {
ip: ip,
mac: $1,
timestamp: Time.now
}
end
end
end
end
end
# Usage
scanner = NetworkScanner.new('192.168.1.0/24')
hosts = scanner.scan(threads: 100)
puts "Found #{hosts.size} active hosts:"
hosts.each do |host|
puts "#{host[:ip].ljust(15)} #{host[:mac]}"
end
# => 192.168.1.1 aa:bb:cc:dd:ee:ff
# => 192.168.1.50 11:22:33:44:55:66
# => 192.168.1.100 22:33:44:55:66:77
DHCP server implementations maintain mappings between MAC addresses and assigned IP addresses. When a DHCP server receives a request from a client, it uses the client's MAC address (from the DHCP packet and potentially from ARP) to determine which IP address to assign or whether to renew an existing lease.
class DHCPARPTracker
def initialize
@leases = {} # MAC => { ip:, expires:, hostname: }
@arp_cache = {} # IP => { mac:, last_seen: }
end
def record_lease(mac_address, ip_address, lease_duration, hostname = nil)
@leases[mac_address] = {
ip: ip_address,
expires: Time.now + lease_duration,
hostname: hostname
}
update_arp_cache(ip_address, mac_address)
end
def update_arp_cache(ip_address, mac_address)
@arp_cache[ip_address] = {
mac: mac_address,
last_seen: Time.now
}
end
def verify_lease(mac_address)
lease = @leases[mac_address]
return nil unless lease
# Check if lease expired
return nil if lease[:expires] < Time.now
# Verify ARP cache consistency
arp_entry = @arp_cache[lease[:ip]]
if arp_entry && arp_entry[:mac] != mac_address
# MAC address conflict detected
return { conflict: true, conflicting_mac: arp_entry[:mac] }
end
lease
end
def detect_rogue_devices
rogue = []
# Get current ARP cache from system
stdout, = Open3.capture3('arp', '-a')
stdout.each_line do |line|
next unless line =~ /\((\d+\.\d+\.\d+\.\d+)\) at ([0-9a-f:]+)/
ip = $1
mac = $2
# Check if IP is in known leases
lease = @leases.values.find { |l| l[:ip] == ip }
if lease && lease[:mac] != mac
rogue << {
ip: ip,
expected_mac: lease[:mac],
actual_mac: mac,
hostname: lease[:hostname]
}
end
end
rogue
end
end
Security Implications
ARP lacks authentication mechanisms, making it vulnerable to spoofing attacks where malicious actors send forged ARP replies to poison cache entries on victim machines. An attacker can claim ownership of any IP address by broadcasting ARP replies associating that IP with the attacker's MAC address. Victim machines accept these replies and update their ARP caches accordingly.
ARP spoofing enables man-in-the-middle attacks within local networks. An attacker positions themselves between two communicating parties by poisoning both machines' ARP caches. Traffic destined for each legitimate party flows through the attacker's machine, allowing interception, modification, or selective forwarding of packets.
# Example: Detecting potential ARP spoofing
class ARPSpoofDetector
def initialize(interface)
@known_mappings = {} # IP => [MAC addresses seen]
@alerts = []
end
def process_arp_packet(packet)
sender_ip = packet[:sender_ip]
sender_mac = packet[:sender_mac]
if @known_mappings.key?(sender_ip)
if !@known_mappings[sender_ip].include?(sender_mac)
# New MAC for existing IP - potential spoofing
@alerts << {
type: :ip_mac_change,
ip: sender_ip,
old_macs: @known_mappings[sender_ip].dup,
new_mac: sender_mac,
timestamp: Time.now,
severity: :high
}
@known_mappings[sender_ip] << sender_mac
end
else
@known_mappings[sender_ip] = [sender_mac]
end
# Check for gratuitous ARP (sender IP == target IP)
if packet[:sender_ip] == packet[:target_ip]
@alerts << {
type: :gratuitous_arp,
ip: packet[:sender_ip],
mac: packet[:sender_mac],
timestamp: Time.now,
severity: :medium
}
end
# Detect rapid ARP replies from different MACs
detect_reply_flood(sender_ip)
end
def alerts
@alerts
end
private
def detect_reply_flood(ip)
recent_packets = @known_mappings[ip]
return unless recent_packets && recent_packets.size > 5
# Check if multiple MACs seen in short timeframe
# (simplified - real implementation would track timestamps)
unique_macs = recent_packets.uniq
if unique_macs.size > 2
@alerts << {
type: :multiple_mac_flood,
ip: ip,
macs: unique_macs,
timestamp: Time.now,
severity: :critical
}
end
end
end
Static ARP entries provide defense against spoofing by preventing cache updates for critical hosts. Administrators manually configure these entries on sensitive systems to establish fixed IP-to-MAC mappings. Static entries persist until explicit removal, resisting ARP reply attacks. However, static entries create management overhead and fail to accommodate legitimate hardware replacements.
Dynamic ARP inspection (DAI), implemented on network switches, validates ARP packets against a trusted database of IP-to-MAC bindings. Switches configured with DAI intercept ARP packets and compare them to DHCP snooping tables or manually configured bindings. Invalid packets are dropped before reaching other network hosts. DAI operates transparently to end systems while providing protection at the infrastructure level.
# Simplified DAI table management
class DynamicARPInspection
def initialize
@trusted_bindings = {} # IP => MAC
@trusted_ports = []
end
def add_trusted_binding(ip_address, mac_address)
@trusted_bindings[ip_address] = mac_address.downcase
end
def add_trusted_port(port_id)
@trusted_ports << port_id
end
def validate_arp_packet(packet, ingress_port)
# Trust packets from trusted ports
return :trusted if @trusted_ports.include?(ingress_port)
sender_ip = packet[:sender_ip]
sender_mac = packet[:sender_mac].downcase
# Check against trusted bindings
if @trusted_bindings.key?(sender_ip)
if @trusted_bindings[sender_ip] == sender_mac
return :valid
else
return {
result: :invalid,
reason: "MAC mismatch",
expected: @trusted_bindings[sender_ip],
received: sender_mac
}
end
end
# No binding exists - depends on policy
# (strict mode would drop, permissive would allow)
return :no_binding
end
end
Gratuitous ARP packets, where a host announces its own IP-to-MAC mapping unsolicited, serve legitimate purposes like detecting IP conflicts and updating cached entries after network changes. However, attackers exploit gratuitous ARP for cache poisoning since receiving hosts update their caches without verification. Monitoring for excessive gratuitous ARP traffic or gratuitous ARP from unexpected sources helps identify attacks.
Rate limiting ARP traffic at network boundaries prevents ARP storms and denial-of-service attacks. Excessive ARP requests can overwhelm switches and hosts, degrading network performance. Implementing rate limits on ARP packet processing protects against both malicious attacks and misconfigured devices that generate excessive legitimate ARP traffic.
Common Pitfalls
ARP cache timeout variations across operating systems cause inconsistent behavior in networked applications. Linux systems typically maintain entries for 60-120 seconds, Windows systems for 2-10 minutes, and other systems implement different policies. Applications assuming specific timeout behavior encounter failures when deployed on different platforms or when cache entries expire unexpectedly.
# Handling cache timeout variations
class ReliableARPResolver
def initialize
@resolution_cache = {}
@cache_duration = 30 # Conservative timeout
end
def resolve_mac(ip_address)
cached = @resolution_cache[ip_address]
# Use cached value if fresh
if cached && (Time.now - cached[:timestamp]) < @cache_duration
return cached[:mac]
end
# Force fresh resolution
mac = perform_resolution(ip_address)
if mac
@resolution_cache[ip_address] = {
mac: mac,
timestamp: Time.now
}
end
mac
end
private
def perform_resolution(ip_address)
# Clear any stale entry
Open3.capture3('arp', '-d', ip_address)
# Generate ARP request via ping
Open3.capture3('ping', '-c', '1', '-W', '2', ip_address)
# Read fresh entry
stdout, = Open3.capture3('arp', '-n', ip_address)
if stdout =~ /([0-9a-f:]{17})/
$1
end
end
end
Incomplete ARP entries occur when a host sends an ARP request but receives no reply. The operating system maintains an incomplete entry for a brief period, typically rejecting outbound packets to that destination. Applications unaware of incomplete entries continue attempting to send data, experiencing silent failures or timeouts. Checking for incomplete entries before critical operations prevents these issues.
Broadcast storms result from misconfigured devices or loops in network topology generating excessive ARP requests. Each broadcast ARP request propagates to all devices on the network segment, and cascading failures occur when devices respond to or regenerate ARP traffic faster than switches can process. Network monitoring tools detecting abnormal ARP traffic volumes help identify and mitigate storms.
ARP table overflow attacks attempt to exhaust switch CAM table capacity by generating ARP traffic for numerous spoofed MAC addresses. Switches fail open when their tables fill, broadcasting all traffic to all ports and enabling promiscuous packet capture. Implementing MAC address limits per port and monitoring table utilization prevents successful overflow attacks.
# Monitoring for ARP anomalies
class ARPAnomalyDetector
def initialize
@request_counts = Hash.new(0)
@window_start = Time.now
@window_duration = 60 # 1 minute windows
end
def process_packet(packet)
current_time = Time.now
# Reset window if needed
if current_time - @window_start > @window_duration
analyze_window
@request_counts.clear
@window_start = current_time
end
if packet[:type] == :request
source = packet[:sender_mac]
@request_counts[source] += 1
end
end
private
def analyze_window
return if @request_counts.empty?
# Detect excessive requests from single source
@request_counts.each do |mac, count|
if count > 100 # Threshold for anomaly
puts "Warning: #{mac} sent #{count} ARP requests in #{@window_duration}s"
end
end
# Detect potential table overflow attempts
unique_sources = @request_counts.size
if unique_sources > 1000
puts "Warning: #{unique_sources} unique MAC addresses seen - possible table overflow attack"
end
end
end
Gratuitous ARP conflicts arise when multiple hosts claim the same IP address, typically due to misconfiguration or malicious activity. Receiving hosts update their caches with the most recent gratuitous ARP, causing connectivity to flip between the conflicting hosts. IP address conflict detection mechanisms, combined with proper DHCP configuration and static IP management, prevent these scenarios.
Mobile device behavior introduces complexity when devices move between networks or reconnect after sleep periods. These devices send gratuitous ARP packets to announce their presence and update cached entries on other hosts. Applications sensitive to IP-to-MAC mapping changes must handle these mobility events gracefully rather than treating them as errors.
Tools & Ecosystem
The arp command-line utility provides basic ARP cache inspection and manipulation across Unix-like systems and Windows. The tool displays cached entries, adds static mappings, and deletes entries. While syntax varies slightly between platforms (arp -a versus arp -an versus ip neighbor), the core functionality remains consistent.
# Cross-platform ARP utility wrapper
class ARPUtil
def self.platform
case RbConfig::CONFIG['host_os']
when /darwin|mac os/
:macos
when /linux/
:linux
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
:windows
else
:unknown
end
end
def self.list_entries
case platform
when :macos, :windows
stdout, = Open3.capture3('arp', '-a')
when :linux
# Try ip neighbor first, fall back to arp
stdout, stderr, status = Open3.capture3('ip', 'neighbor', 'show')
if !status.success?
stdout, = Open3.capture3('arp', '-a')
end
end
parse_entries(stdout, platform)
end
private
def self.parse_entries(output, platform)
entries = []
output.each_line do |line|
case platform
when :linux
# Format: 192.168.1.1 dev eth0 lladdr aa:bb:cc:dd:ee:ff REACHABLE
if line =~ /^(\S+) dev (\S+) lladdr ([0-9a-f:]+) (\S+)/
entries << {
ip: $1,
interface: $2,
mac: $3,
state: $4.downcase.to_sym
}
end
when :macos, :windows
# Format: hostname (192.168.1.1) at aa:bb:cc:dd:ee:ff on en0 [ethernet]
if line =~ /\((\d+\.\d+\.\d+\.\d+)\) at ([0-9a-f:]+)/
entries << {
ip: $1,
mac: $2
}
end
end
end
entries
end
end
Arping generates ARP requests to specific IP addresses and reports responses, similar to ICMP ping but operating at the data link layer. The tool verifies host presence when ICMP is blocked, detects duplicate IP addresses, and measures response times. Arping requires raw socket access and typically needs elevated privileges.
Tcpdump and Wireshark capture and analyze network traffic including ARP packets. These tools reveal ARP request-reply patterns, identify spoofing attempts, and diagnose cache inconsistencies. Applying ARP-specific filters (arp in tcpdump, arp in Wireshark) isolates ARP traffic for focused analysis.
The arp-scan tool performs rapid network discovery by sending ARP requests to all addresses in specified ranges. The tool provides features beyond basic arping including customizable timing, MAC address vendor identification, and output formatting options. Security professionals use arp-scan for network reconnaissance and asset inventory.
# Using arp-scan results in Ruby
class NetworkInventory
def self.scan_network(network_cidr)
stdout, stderr, status = Open3.capture3(
'arp-scan',
'--interface=eth0',
'--numeric',
'--plain',
network_cidr
)
unless status.success?
raise "arp-scan failed: #{stderr}"
end
devices = []
stdout.each_line do |line|
if line =~ /^(\d+\.\d+\.\d+\.\d+)\s+([0-9a-f:]+)\s+(.+)$/
devices << {
ip: $1,
mac: $2,
vendor: $3.strip
}
end
end
devices
end
end
The arpwatch daemon monitors networks for ARP activity, recording observed IP-to-MAC pairings and sending notifications when changes occur. The tool maintains a database of mappings and alerts administrators to new hosts, address changes, and flip-flops. Arpwatch helps detect spoofing attacks and track network changes over time.
Ruby gems providing network functionality include PacketFu for packet creation and parsing, Pcaprub for packet capture, and Net::Ping for network reachability testing. These libraries enable building custom ARP tools and integrating ARP functionality into larger applications.
Switch management interfaces expose ARP table contents and Dynamic ARP Inspection configuration. SNMP queries retrieve MAC address tables and port status. RESTful APIs on modern switches provide programmatic access to network state including learned ARP bindings.
Reference
ARP Packet Structure
| Field | Length | Description |
|---|---|---|
| Hardware Type | 2 bytes | Network link protocol type (1 for Ethernet) |
| Protocol Type | 2 bytes | Protocol being resolved (0x0800 for IPv4) |
| Hardware Address Length | 1 byte | MAC address length in bytes (6 for Ethernet) |
| Protocol Address Length | 1 byte | Protocol address length (4 for IPv4) |
| Operation | 2 bytes | Request (1) or Reply (2) |
| Sender Hardware Address | 6 bytes | MAC address of sender |
| Sender Protocol Address | 4 bytes | IP address of sender |
| Target Hardware Address | 6 bytes | MAC address of target |
| Target Protocol Address | 4 bytes | IP address of target |
ARP Cache Entry States
| State | Description | Typical Duration |
|---|---|---|
| REACHABLE | Valid entry recently confirmed | 60-300 seconds |
| STALE | Entry not recently confirmed but usable | Until next use |
| DELAY | First packet sent, waiting for confirmation | 5 seconds |
| PROBE | Actively probing for reachability | 1 second per probe |
| INCOMPLETE | ARP request sent, no reply received | 3-5 seconds |
| FAILED | No response after multiple probes | Immediate removal |
| PERMANENT | Static entry | Until manual removal |
Common ARP Command Operations
| Operation | Linux | macOS | Windows |
|---|---|---|---|
| Display cache | arp -a or ip neighbor | arp -a | arp -a |
| Display numeric | arp -n or ip -n neighbor | arp -an | arp -a |
| Delete entry | arp -d IP or ip neighbor del IP | arp -d IP | arp -d IP |
| Add static entry | arp -s IP MAC or ip neighbor add IP lladdr MAC dev IFACE | arp -s IP MAC | arp -s IP MAC |
| Flush all entries | ip neighbor flush all | arp -ad (requires root) | netsh interface ip delete arpcache |
Ruby ARP Libraries and Gems
| Library | Purpose | Installation |
|---|---|---|
| PacketFu | Packet manipulation and creation | gem install packetfu |
| Pcaprub | Packet capture bindings | gem install pcaprub |
| Net::Ping | Network reachability testing | gem install net-ping |
| Open3 | Execute system commands safely | Built-in standard library |
| Socket | Low-level socket operations | Built-in standard library |
Security Monitoring Thresholds
| Metric | Normal Range | Alert Threshold | Critical Threshold |
|---|---|---|---|
| ARP requests per host | 10-50/min | 100/min | 500/min |
| Unique MAC addresses | Actual device count | 2x device count | 10x device count |
| IP-MAC changes per hour | 0-5 | 10 | 50 |
| Gratuitous ARP per host | 0-2/hour | 10/hour | 50/hour |
| Broadcast ARP ratio | 80-95% | 98% | 99.5% |
ARP Spoofing Detection Indicators
| Indicator | Description | Response Priority |
|---|---|---|
| MAC address change | Existing IP associates with different MAC | High |
| Duplicate IP | Multiple MACs claim same IP address | Critical |
| Gratuitous ARP flood | Excessive unsolicited ARP announcements | High |
| ARP reply without request | Reply received when no request sent | Medium |
| Reply from unexpected source | Response from MAC other than target | High |
| Conflicting gateway MAC | Default gateway MAC differs across hosts | Critical |
Platform-Specific ARP Cache Timeouts
| Platform | Dynamic Entry | Incomplete Entry | Static Entry |
|---|---|---|---|
| Linux | 60-300s (configurable) | 1-3s | Permanent |
| macOS | 1200s | 3s | Permanent |
| Windows 10/11 | 120-600s | 3s | Permanent |
| FreeBSD | 1200s | 2s | Permanent |
| Cisco IOS | 14400s | N/A | Permanent |