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

// libjoin.
#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❌