Project Overview & Difficulty Rating

When building network projects with Raspberry Pi hardware, visibility into local subnet traffic is the foundational first step. This project builds a dedicated ARP (Address Resolution Protocol) presence scanner that sweeps your local subnet, identifies active MAC/IP pairs, and pushes state changes to an MQTT broker for integration with Home Assistant or custom dashboards.

Target Board Variant: This guide and code explicitly target the Raspberry Pi 5 (4GB model) running Raspberry Pi OS Bookworm (64-bit). The Pi 5’s PCIe 2.0 interface and updated BCM2712 SoC handle raw packet crafting via Scapy significantly faster than the Pi 4, reducing sweep times on a /24 subnet from ~4 seconds to under 1.5 seconds.

Difficulty: Intermediate (3/5)
Time to Build: 45 minutes
Core Skills: Linux networking, Python raw sockets, MQTT pub/sub, GPIO hardware mapping.

Hardware Spec Sheet & Pin Mapping

Do not rely on underpowered supplies for network monitoring; dropped packets during high-load ARP sweeps often trace back to brownouts on the USB bus or SoC throttling. Below is the exact bill of materials with estimated 2026 street pricing.

ComponentExact VariantEst. Price (2026)
Compute BoardRaspberry Pi 5 (4GB RAM)$60.00
Power SupplyOfficial 27W USB-C PD Power Supply$12.00
ThermalOfficial Active Cooler (PWM driven)$5.00
StorageSanDisk Extreme 32GB A2 U3 microSD$14.00
Network InterfaceTP-Link UE300 USB 3.0 Gigabit Ethernet (for dedicated span port)$18.00
Status LED5mm Red LED + 330Ω Resistor$0.10
Reset SwitchMomentary tactile pushbutton (6x6mm)$0.05

GPIO Pin Mapping

We map physical indicators to give the headless Pi a physical debug interface on the workbench. Use the Broadcom (BCM) pin numbering scheme.

FunctionBCM GPIOPhysical PinWiring Notes
Status LED (Anode)1711Series 330Ω resistor to LED
Status LED (Cathode)GND9Common ground
Reset Button (Signal)2713Internal pull-up enabled in code
Reset Button (GND)GND14Switches to ground on press

Step-by-Step Build & Network Configuration

  1. Flash and Boot: Use Raspberry Pi Imager to flash Bookworm 64-bit to the SanDisk A2 card. Enable SSH and set your Wi-Fi/Ethernet credentials in the OS customization menu.
  2. Install Dependencies: SSH into the Pi and install the required Python libraries and the Mosquitto broker.
    sudo apt update
    sudo apt install mosquitto mosquitto-clients python3-pip python3-venv
    mkdir ~/arp-scanner && cd ~/arp-scanner
    python3 -m venv venv
    source venv/bin/activate
    pip install scapy paho-mqtt gpiozero
  3. Grant Raw Socket Permissions: Scapy requires root privileges to craft raw Ethernet frames. Instead of running the entire script as root (a security risk), grant the Python binary the CAP_NET_RAW capability.
    sudo setcap cap_net_raw+ep $(which python3)
  4. Configure MQTT Broker: Edit the Mosquitto config to allow local connections. Create /etc/mosquitto/conf.d/local.conf and add:
    listener 1883 127.0.0.1
    allow_anonymous true
    Restart the service: sudo systemctl restart mosquitto.
  5. Wire the GPIO: Connect the 330Ω resistor to BCM 17, then to the LED anode. Connect the cathode to GND. Wire the tactile switch between BCM 27 and GND.
Bench Tip: If you are plugging the TP-Link USB NIC into a managed switch’s SPAN/Mirror port to passively monitor traffic rather than actively scanning, you must disable the IP stack on that interface to prevent the Pi from responding to broadcast traffic on the monitor VLAN. Run: sudo ip link set eth1 promisc on and ensure it has no DHCP assignment in /etc/dhcpcd.conf or NetworkManager.

The Python ARP Scanner Code

The following script sweeps the local subnet using ARP requests, compares the results against a known state dictionary, and publishes new arrivals or departures to the MQTT broker. It includes hardware pin definitions and robust error handling for network drops.

#!/usr/bin/env python3
import time
import sys
import logging
import json
from scapy.all import ARP, Ether, srp
import paho.mqtt.client as mqtt
from gpiozero import LED, Button
from signal import pause

# --- Pin Definitions ---
STATUS_LED = LED(17)
RESET_BTN = Button(27, pull_up=True)

# --- Configuration ---
TARGET_SUBNET = "192.168.1.0/24"
MQTT_BROKER = "127.0.0.1"
MQTT_PORT = 1883
MQTT_TOPIC = "network/presence/arp_scan"
SCAN_INTERVAL = 60  # seconds

# State tracking
known_devices = {}

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

def on_connect(client, userdata, flags, rc, properties=None):
    if rc == 0:
        logging.info("Connected to MQTT Broker")
        STATUS_LED.blink(on_time=0.1, off_time=0.1, n=3)
    else:
        logging.error(f"MQTT Connection failed with code {rc}")

def on_disconnect(client, userdata, rc):
    logging.warning("Disconnected from MQTT. Attempting auto-reconnect...")
    STATUS_LED.on() # Solid LED indicates fault

def scan_network():
    """Crafts and sends ARP requests to the target subnet."""
    global known_devices
    STATUS_LED.on()
    
    try:
        arp_request = ARP(pdst=TARGET_SUBNET)
        broadcast = Ether(dst="ff:ff:ff:ff:ff:ff")
        packet = broadcast / arp_request
        
        # srp sends and receives at Layer 2, timeout=2 prevents hanging on dead subnets
        result = srp(packet, timeout=2, verbose=False)[0]
        
        current_devices = {}
        for sent, received in result:
            current_devices[received.psrc] = received.hwsrc
            
        # Detect new arrivals
        for ip, mac in current_devices.items():
            if ip not in known_devices:
                logging.info(f"NEW DEVICE: {ip} ({mac})")
                client.publish(MQTT_TOPIC, json.dumps({"action": "join", "ip": ip, "mac": mac}))
                
        # Detect departures
        for ip, mac in list(known_devices.items()):
            if ip not in current_devices:
                logging.info(f"DEVICE LEFT: {ip} ({mac})")
                client.publish(MQTT_TOPIC, json.dumps({"action": "leave", "ip": ip, "mac": mac}))
                
        known_devices = current_devices
        STATUS_LED.off()
        
    except PermissionError as e:
        logging.critical(f"Raw socket permission denied: {e}. Did you run setcap?")
        sys.exit(1)
    except OSError as e:
        logging.error(f"Network unreachable: {e}. Check interface status.")
        STATUS_LED.blink(on_time=0.5, off_time=0.5)
    except Exception as e:
        logging.error(f"Unexpected scan error: {e}")

def hardware_reset():
    logging.warning("Hardware reset button pressed. Clearing state.")
    global known_devices
    known_devices = {}
    client.publish(MQTT_TOPIC, json.dumps({"action": "system_reset"}))

# --- Main Execution ---
if __name__ == "__main__":
    client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
    client.on_connect = on_connect
    client.on_disconnect = on_disconnect
    
    try:
        client.connect(MQTT_BROKER, MQTT_PORT, 60)
        client.loop_start()
    except ConnectionRefusedError as e:
        logging.critical(f"MQTT Broker refused connection: {e}")
        sys.exit(1)

    RESET_BTN.when_pressed = hardware_reset

    logging.info(f"Starting ARP sweep on {TARGET_SUBNET}...")
    try:
        while True:
            scan_network()
            time.sleep(SCAN_INTERVAL)
    except KeyboardInterrupt:
        logging.info("Shutting down gracefully.")
        client.loop_stop()
        STATUS_LED.off()

Debugging: First Three Things to Check

When network projects fail on embedded Linux, the fault is almost always at the intersection of Python’s virtual environment and Linux kernel security modules. If your script crashes on startup, check these three exact error strings in this order.

1. "PermissionError: [Errno 1] Operation not permitted"

The Cause: Scapy uses raw sockets to craft Layer 2 Ethernet frames. Standard Linux users cannot open raw sockets. You either forgot to run setcap, or you are running the script from a virtual environment (venv) where the Python binary path differs from the system Python.

The Fix: If using a venv, apply the capability to the venv’s Python binary, not the system one:

sudo setcap cap_net_raw+ep ~/arp-scanner/venv/bin/python3
Verify it took effect with getcap ~/arp-scanner/venv/bin/python3.

2. "ConnectionRefusedError: [Errno 111] Connection refused"

The Cause: The Mosquitto MQTT broker is either not running, or it is blocking anonymous local connections because the default Bookworm Mosquitto package ships with strict ACLs out of the box.

The Fix: Check the service status: systemctl status mosquitto. If it’s active but refusing connections, ensure your /etc/mosquitto/conf.d/local.conf file contains allow_anonymous true and listener 1883 127.0.0.1, then run sudo systemctl restart mosquitto.

3. "OSError: [Errno 101] Network is unreachable"

The Cause: Scapy cannot determine the default route to send the ARP broadcast. This happens if your Pi is connected to a switch but hasn’t pulled a DHCP lease, or if you are targeting a subnet (e.g., 192.168.1.0/24) that doesn’t match the Pi’s actual IP configuration.

The Fix: Run ip route. Ensure there is a default route or a specific route to your target subnet. If the interface is down, bring it up with sudo ip link set eth0 up and verify your TARGET_SUBNET variable matches your actual network topology.

Extending and Simplifying the Build

Not every deployment requires a full MQTT stack. Here is how to scale the project up for enterprise visibility or down for standalone bench testing.

How to Simplify (Standalone Logging)

If you just want to log MAC addresses to the Pi’s local storage without a broker, strip out the paho-mqtt library entirely. Replace the client.publish() calls with Python’s built-in logging module configured to write to a rotating file handler, or use rsyslog to push the events to /var/log/arp_scanner.log. This eliminates the broker dependency and reduces RAM usage by roughly 15MB.

How to Extend (MAC OUI Lookup & Home Assistant)

To turn raw MAC addresses into human-readable device names, integrate a local OUI (Organizationally Unique Identifier) database. 1. Download the IEEE OUI text file and parse it into a Python dictionary or SQLite database. 2. In the scan_network() loop, slice the first 6 characters of the mac string (e.g., mac[:8]) and query your database to append the manufacturer name (e.g., "Apple", "Espressif") to the MQTT payload. 3. For Home Assistant, format the MQTT payload to match the MQTT Discovery schema, allowing the Pi to automatically register binary sensors for every device it detects on the network.

Frequently Asked Questions

What is the best Raspberry Pi for network monitoring projects?

For passive packet sniffing or high-speed ARP sweeping, the Raspberry Pi 5 (4GB or 8GB) is the definitive choice in 2026. Its BCM2712 SoC handles interrupt loads from gigabit NICs far better than the Pi 4’s BCM2711. If you are building a dedicated router/firewall (like OPNsense/pfSense alternatives), you are better served by x86 mini-PCs with native multi-gigabit Intel NICs, as the Pi 5 still relies on USB 3.0 or PCIe 2.0 (which requires a specialized HAT) for secondary network interfaces.

How to run Raspberry Pi network projects headless?

Use the Raspberry Pi Imager’s hidden "OS Customization" menu (Ctrl+Shift+X or the gear icon) before flashing. Check "Enable SSH" and select "Use password authentication". Set a strong password. Once booted, SSH in via ssh username@raspberrypi.local. To ensure your Python script survives reboots and terminal disconnects, create a systemd service file in /etc/systemd/system/arp-scanner.service rather than using screen or tmux.

Are Raspberry Pi network projects secure from VLAN hopping?

Out of the box, no. If your Pi is connected to a trunk port on a managed switch and you are running raw socket scripts, a misconfigured script could inadvertently tag packets with the wrong 802.1Q VLAN ID, potentially crossing VLAN boundaries. To secure the Pi in an enterprise environment, ensure the switch port is configured as an Access Port assigned to a dedicated monitoring VLAN, or if using a SPAN port, ensure the SPAN destination port has 802.1Q tagging explicitly disabled at the switch level. Refer to your switch vendor's documentation on SPAN port security best practices.