ARP#

Join provides an Arp class implementation for discovering MAC addresses from IPv4 addresses on local networks.

The Arp class handles:

  • cache queries — fast lookups via NeighborManager
  • active requests — broadcast ARP queries when the cache has no valid entry
  • cache management — add and remove static entries via NeighborManager
  • automatic fallback — tries the neighbor cache first, then sends a request

⚠️ ARP is an IPv4-only protocol. For IPv6 use NeighborManager directly (NDP is handled by the kernel).


Creating an ARP instance#

#include <join/arp.hpp>

using namespace join;

// Uses NeighborManager::instance() internally
Arp arp("eth0");

// Inject a custom NeighborManager
NeighborManager myManager;
Arp arp("eth0", &myManager);

The instance is bound to a specific network interface. It shares the reactor of the injected (or singleton) NeighborManager.

Arp is neither copyable nor movable.


Resolving MAC addresses#

get() checks the neighbor cache first. If no valid entry is found (NUD_VALID), it sends an ARP request:

IpAddress target("192.168.1.100");
MacAddress mac = arp.get(target);

if (!mac.isWildcard())
{
    std::cout << "MAC: " << mac.toString() << "\n";
}
else
{
    std::cerr << "Failed: " << lastError.message() << "\n";
}

With a custom timeout:

MacAddress mac = arp.get(target, std::chrono::milliseconds(500));

Static method#

MacAddress mac = Arp::get("eth0", IpAddress("192.168.1.100"));

// Custom timeout
MacAddress mac = Arp::get("eth0", IpAddress("192.168.1.100"),
                           std::chrono::milliseconds(500));

ARP requests#

request() always sends a broadcast ARP packet on the wire, regardless of the cache:

IpAddress target("192.168.1.100");
MacAddress mac = arp.request(target);

if (!mac.isWildcard())
{
    std::cout << "Discovered: " << mac.toString() << "\n";
}

With a custom timeout:

MacAddress mac = arp.request(target, std::chrono::milliseconds(200));

Static method#

MacAddress mac = Arp::request("eth0", IpAddress("192.168.1.100"));

MacAddress mac = Arp::request("eth0", IpAddress("192.168.1.100"),
                               std::chrono::milliseconds(200));

The default timeout is 5 seconds.

The method:

  • Opens a raw socket on the interface
  • Attaches a BPF filter to accept only ARP replies
  • Broadcasts an ARP request packet
  • Blocks until a matching reply arrives or the timeout expires
  • Closes the socket on return

Neighbor cache queries#

cache() looks up an entry in NeighborManager without sending any packet. Only entries in a valid NUD state (NUD_REACHABLE, NUD_PERMANENT, NUD_STALE, NUD_DELAY, NUD_PROBE, NUD_NOARP) are returned:

IpAddress target("192.168.1.100");
MacAddress mac = arp.cache(target);

if (!mac.isWildcard())
{
    std::cout << "Cached: " << mac.toString() << "\n";
}
else
{
    std::cerr << "No valid cache entry\n";
}

Static method#

MacAddress mac = Arp::cache("eth0", IpAddress("192.168.1.100"));

Managing cache entries#

Add a static entry#

Adds a permanent neighbor entry via NeighborManager (NUD_PERMANENT):

MacAddress mac("00:11:22:33:44:55");
IpAddress  ip("192.168.1.100");

if (arp.add(mac, ip) == -1)
{
    std::cerr << "Failed: " << lastError.message() << "\n";
}

Static method#

Arp::add("eth0", MacAddress("00:11:22:33:44:55"), IpAddress("192.168.1.100"));

Remove an entry#

Removes an entry from the neighbor cache via NeighborManager:

if (arp.remove(IpAddress("192.168.1.100")) == -1)
{
    std::cerr << "Failed: " << lastError.message() << "\n";
}

Static method#

Arp::remove("eth0", IpAddress("192.168.1.100"));

Special cases#

Local interface address#

Querying the local IP returns the local MAC directly without any ARP:

// Returns local MAC immediately
MacAddress localMac = arp.get(IpAddress::ipv4Address("eth0"));

Error handling#

All methods return a wildcard MacAddress (or -1 for add/remove) on failure. Check lastError:

MacAddress mac = arp.request(target);
if (mac.isWildcard())
{
    if (lastError == std::errc::no_such_device_or_address)
        std::cerr << "No ARP reply received (host down or filtered)\n";
    else if (lastError == Errc::InvalidParam)
        std::cerr << "IPv4 address required\n";
    else
        std::cerr << "Error: " << lastError.message() << "\n";
}

Common error codes:

CodeCause
std::errc::no_such_device_or_addressNo ARP reply, timeout, or no valid cache entry
Errc::InvalidParamAddress is not IPv4
Errc::TimedOutRequest timed out

Permissions#

OperationRequired capability
Cache query (cache())None
ARP request (request())CAP_NET_RAW
Add/remove entry (add(), remove())CAP_NET_ADMIN
# Grant both capabilities
sudo setcap cap_net_raw,cap_net_admin+ep ./your_program

Details#

BPF filter#

Incoming packets are filtered to accept only ARP replies:

struct sock_filter code[] = {
    { 0x28, 0, 0, 0x0000000c },  // Load EtherType
    { 0x15, 0, 3, 0x00000806 },  // Jump if not ARP
    { 0x28, 0, 0, 0x00000014 },  // Load ARP opcode
    { 0x15, 0, 1, 0x00000002 },  // Jump if not reply
    { 0x6,  0, 0, 0x00040000 },  // Accept
    { 0x6,  0, 0, 0x00000000 },  // Reject
};

Best practices#

  • Use get() for general MAC resolution — it avoids unnecessary network traffic.
  • Use cache() when you need a non-intrusive lookup and a stale result is acceptable.
  • Use request() only when you need fresh data unconditionally.
  • Add static entries with add() for critical infrastructure hosts to avoid resolution delays.
  • Remove stale static entries with remove() when decommissioning hosts.
  • Keep ARP timeouts short in time-sensitive paths.

Limitations#

  • IPv4 only — use NeighborManager for IPv6 NDP entries.
  • Local network only — cannot resolve addresses across routers.
  • No retries — a single request is sent; retry logic must be implemented by the caller.

Summary#

MethodCache lookupNetwork requestNotes
get()✅ (fallback)Recommended for general use
cache()Fast, no network traffic
request()Always sends a packet
add()Adds permanent static entry
remove()Removes entry from cache
FeatureSupported
Cache queries
ARP requests
Add cache entries
Remove cache entries
Custom timeout
Custom NeighborManager
Static methods
IPv4 support
IPv6 support