dmarp is a DMOD library
module that resolves an IPv4 address to a MAC address on a
directly-connected link, using ARP (RFC 826) request/reply frames - the
piece that turns "send to 192.168.1.1" into an actual destination MAC for
an Ethernet header, without every caller having to hand-roll ARP itself.
It sits directly on top of dmnetif
(dmnetif_send() to transmit a request, dmnetif_get_mac_address()/
_get_ip_address() to build one) and keeps a cache so repeated
resolutions of the same destination don't send a fresh request every time.
┌──────────────────────────────────────────────┐
│ DMNETBRIDGE │
│ (routing/ARP/frame I/O for dmip, driven │
│ by networkd) │
├──────────────────────────────────────────────┤
│ DMARP │
│ resolve (cache + request/reply exchange), │
│ cache lookup/insert/remove/count │
├──────────────────────────────────────────────┤
│ DMNETIF │
│ named interfaces, send/receive one frame │
├──────────────────────────────────────────────┤
│ DMROUTE │
│ shared dmroute_addr_t type │
└──────────────────────────────────────────────┘
IPv6 is out of scope: IPv6 neighbor discovery uses NDP over ICMPv6, an
entirely different protocol, not ARP - every dmarp function only ever
deals with dmroute_family_v4 addresses.
dmarp_resolve()does not read frames off the wire itself. Oncenetworkdis running,dmnetbridge_handle_netif_rx()is the only code path allowed to calldmnetif_receive()on a given interface - a second concurrent reader would race it and starve. Instead,dmarp_resolve()sends its request and then waits on a signal thatdmarp_note_frame()raises whenever any ARP frame comes in through dmnetbridge, re-checking the cache each time it wakes.- Opportunistic learning, not just replies to our own requests.
dmarp_note_frame()is fed every frame dmnetbridge's RX pump reads off an interface. Any well-formed ARP request or reply - not only a reply to a request we sent - has its sender's(interface, IP) -> MACmapping cached unconditionally. That's what lets repeated sends to a peer we've merely heard from (e.g. its own broadcast ARP to someone else) skip a fresh round trip entirely. - Only one request per
dmarp_resolve()call - no built-in retry. A caller that wants retry behavior just callsdmarp_resolve()again after a timeout; the second call is its own independent request/wait cycle. - The cache is keyed by interface name, not by
dmnetif_iface_thandle - the same reasoning dmroute uses for its own routes: a stored handle could go stale across interface churn (unregister/re-register), a name can't. This does mean a re-registered interface with the same name inherits whatever was cached for that name before. - Every cache entry expires after
DMARP_CACHE_TTL_MS(60 seconds), applied uniformly whether the entry came from a real ARP reply or a manualdmarp_cache_insert()- there is no separate "static/permanent" entry concept. A hardcoded mapping needs to be re-inserted periodically to stay valid, same as anything else in the cache. - A resolving interface with no IP address of its own yet is not an
error.
dmarp_resolve()still sends a request with an all-zero sender protocol address - a real host can legitimately ARP before it has an address of its own (e.g. during DHCP).
See docs/dmarp.md for the full rationale and docs/api-reference.md for the complete API.
dmarp is a standalone DMOD module: its CMakeLists.txt fetches dmod
itself via CMake's FetchContent (defaulting to the develop branch), so
no other repository needs to be checked out first.
mkdir -p build
cd build
cmake ..
cmake --build .This builds both the dmarp library module and its test_dmarp test
binary (see Testing below). Pass -DDMOD_DIR=/path/to/local/dmod
to build against a local dmod checkout instead of fetching it from GitHub.
make DMOD_MODE=DMOD_MODULE DMOD_DIR=/path/to/dmodThe Makefile requires an existing dmod checkout (there is no
FetchContent equivalent for Make) and builds the dmarp module itself,
not the test suite.
Tests are a plain DMOD test module built with dmod_add_test():
tests/dmarp_test.c registers each test case with DMOD_TEST_STEP() and
runs against two fixture interfaces ("test0"/"test1") registered
against "/dev/null" - real enough for the underlying file open to succeed
without needing an actual driver behind it (same pattern
dmnetif's own tests use).
Despite being built as a native ELF binary, test_dmarp is a DMOD module
like any other and must be run through dmod_loader, not invoked directly
(./tests/test_dmarp segfaults - it expects the loader's runtime around
it, not a bare main()). dmf-get install first resolves test_dmarp's
own dependency closure (dmnetif, dmroute, ...) into DMOD_DMF_DIR so
the loader can find every module test_dmarp.dmf needs at load time -
exactly what CI does:
export DMOD_DMF_DIR=$(pwd)/build/dmf
dmf-get install -d ${DMOD_DMF_DIR}/test_dmarp-local.dmd -y
dmod_loader build/dmf/test_dmarp.dmfIt discovers and runs every DMOD_TEST_STEP() automatically and prints a
Results: X/Y passed summary.
tests/dmarp_test.c covers:
- Cache - lookup on an unknown entry, insert/lookup round-trip, insert
replacing an existing entry without growing the cache, independent
entries for the same IP on two different interfaces, remove (present and
absent entry), and invalid-argument (
NULL/wrong family) rejection for every cache function. dmarp_resolve()- the cache-hit path (returns immediately, no frame sent),NULL/wrong-family argument rejection, and the-ENODEVpath when the interface has no real driver behind it.dmarp_note_frame()- caching the sender from both a request and a reply, waking a cache lookup so a pendingdmarp_resolve()finds the entry, ignoring a non-ARP ethertype, ignoring a too-short frame, andNULL-argument safety.
Since the fixture interfaces have no real driver behind them, they can
never actually go "up" or send/receive a real frame - dmarp_resolve()'s
reply-wait/timeout path itself is only exercised indirectly (through
dmarp_note_frame()'s cache side effect); real end-to-end coverage needs a
real network driver behind dmnetif_register().
The common case: resolve the next-hop IP to a MAC address, then build the Ethernet frame around it.
#include "dmarp.h"
#include "dmnetif.h"
int send_to(dmnetif_iface_t iface, const dmroute_addr_t* next_hop,
const void* payload, size_t payload_len)
{
dmnetif_mac_addr_t dst_mac;
int ret = dmarp_resolve(iface, next_hop, &dst_mac, DMARP_DEFAULT_TIMEOUT_MS);
if (ret != 0)
{
return ret; /* -EINVAL / -ENODEV / -EIO / -ETIMEDOUT */
}
/* ... build an Ethernet frame addressed to dst_mac and dmnetif_send() it ... */
return 0;
}A cache hit returns immediately without sending anything; a cache miss
sends one ARP request and blocks up to timeout_ms waiting for a reply to
arrive through dmarp_note_frame() (see Key design points).
Use dmarp_cache_lookup() when you want to know "do we already know this
destination?" without risking a send or a wait - e.g. to decide whether to
show a destination as "reachable" in a UI, or to skip work entirely when
nothing is cached yet.
#include "dmarp.h"
dmnetif_mac_addr_t mac;
if (dmarp_cache_lookup(iface, &dst_ip, &mac))
{
/* already known - use mac directly */
}
else
{
/* not cached (or expired) - fall back to dmarp_resolve() if blocking is OK */
}dmarp_cache_insert() is what dmarp_resolve() calls internally after a
successful reply, but it's also useful directly - for example to hardcode
a gateway's MAC address at startup so the very first packet to it doesn't
pay for an ARP round trip, or to set up a fixture in a test.
#include "dmarp.h"
dmroute_addr_t gateway_ip = { .family = dmroute_family_v4, .addr.v4 = { 192, 168, 1, 1 } };
dmnetif_mac_addr_t gateway_mac = { { 0x00, 0x1a, 0x2b, 0x3c, 0x4d, 0x5e } };
dmarp_cache_insert(iface, &gateway_ip, &gateway_mac);Like any other entry, a manually-seeded one expires after
DMARP_CACHE_TTL_MS and needs to be re-inserted (or left to be re-resolved
normally) to stay valid.
If something detects an address conflict (e.g. a duplicate-IP notification,
or a peer that stopped responding on its previously-known MAC),
dmarp_cache_remove() forces the next dmarp_resolve() for that
destination to send a fresh request instead of waiting out the TTL.
#include "dmarp.h"
dmarp_cache_remove(iface, &suspect_ip);dmarp_note_frame() is meant to be called for every frame read off an
interface, not just ones a pending dmarp_resolve() is waiting on - this
is how dmnetbridge's
per-interface RX pump feeds it:
#include "dmarp.h"
#include "dmnetif.h"
void handle_netif_rx(dmnetif_iface_t iface)
{
uint8_t frame[2048]; /* large enough for one Ethernet frame */
while (dmnetif_is_present(iface))
{
size_t frame_len = dmnetif_receive(iface, frame, sizeof(frame));
if (frame_len == 0)
continue;
dmarp_note_frame(iface, frame, frame_len); /* opportunistic ARP learning */
/* ... hand frame off to whatever else needs to see it (IP stack, etc.) ... */
}
}Frames that aren't a well-formed ARP request or reply are silently ignored, so this is safe to call unconditionally on every received frame regardless of its actual ethertype.
| Constant | Value | Description |
|---|---|---|
DMARP_CACHE_TTL_MS |
60000 | How long a cache entry stays valid, however it got there. |
DMARP_DEFAULT_TIMEOUT_MS |
1000 | Suggested timeout_ms for dmarp_resolve() when you don't have a strong opinion. |
| Function | Description |
|---|---|
dmarp_resolve(iface, ip, mac, timeout_ms) |
Resolve ip to a MAC address on iface. Cache hit: immediate. Cache miss: sends one ARP request, then waits up to timeout_ms for a reply. |
dmarp_note_frame(iface, frame, frame_len) |
Feed one received frame to dmarp for opportunistic ARP learning and to wake any pending dmarp_resolve() call. |
| Function | Description |
|---|---|
dmarp_cache_lookup(iface, ip, mac) |
Look up a cached entry without ever sending a request or blocking. |
dmarp_cache_insert(iface, ip, mac) |
Add or replace a cache entry - usable directly to seed a known mapping. |
dmarp_cache_remove(iface, ip) |
Remove one entry, if present. Safe when no matching entry exists. |
dmarp_cache_count() |
Number of entries currently in the cache (expired or not). |
Every address is a dmroute_addr_t from
dmroute; MAC addresses use
dmnetif_mac_addr_t from
dmnetif. Full
parameter/return documentation (including every error code) lives in
docs/api-reference.md.
See the docs/ directory:
- dmarp.md - Overview and architecture
- api-reference.md - Complete API documentation
View documentation using dmf-man dmarp.
dmnetif-dmnetif_send()to transmit a request,dmnetif_get_mac_address()/_get_ip_address()to build one,dmnetif_get_name()to key cache entries.dmarp_resolve()never callsdmnetif_receive()itself - see Key design points.dmroute- the shareddmroute_addr_ttype every address field uses.dmlist- backs the ARP cache.dmosi- the mutex guarding the cache,dmosi_get_tick_count()for cache TTLs, and the semaphoredmarp_resolve()waits on anddmarp_note_frame()posts.
dmarp/
├── docs/ # Documentation (markdown format)
│ ├── README.md
│ ├── dmarp.md
│ └── api-reference.md
├── include/ # Public headers
│ └── dmarp.h
├── src/
│ └── dmarp.c
├── tests/
│ ├── CMakeLists.txt
│ └── dmarp_test.c
├── CMakeLists.txt
├── Makefile
├── manifest.dmm
└── dmarp.dmr
Patryk Kubiak
MIT License (see LICENSE file for details)