diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8321b4d..94bc607 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -98,3 +98,10 @@ dmod_link_modules(${DMOD_MODULE_NAME}
enable_testing()
add_subdirectory(tests)
+
+# Application-type DMOD modules built as CLI tools around dmicmp's API.
+# Added after dmod_add_library() above so ${DMOD_MODULE_NAME}_if already
+# exists - tools/ping links it directly, the same way tests/CMakeLists.txt
+# does, rather than fetching dmicmp's own headers back via
+# dmod_link_modules() as if it were an external module.
+add_subdirectory(tools)
diff --git a/README.md b/README.md
index 1daede9..6d8998b 100644
--- a/README.md
+++ b/README.md
@@ -77,6 +77,13 @@ dmicmp_register_echo_listener(my_identifier, my_echo_reply_handler);
dmicmp_v4_send_echo_request(&dst, my_identifier, 1, NULL, 0, DMARP_DEFAULT_TIMEOUT_MS);
```
+## Tools
+
+- **[tools/ping](tools/ping/README.md)** - CLI to send ICMPv4 Echo
+ Requests to a host and report the replies, built on the API below.
+ It's its own Application-type DMOD module (`ping`), released alongside
+ `dmicmp` itself in every tagged release.
+
## API
| Function | Description |
@@ -112,6 +119,9 @@ dmicmp/
├── tests/
│ ├── CMakeLists.txt
│ └── dmicmp_test.c
+├── tools/
+│ ├── CMakeLists.txt
+│ └── ping/ # ping CLI - its own Application-type module, see tools/ping/README.md
├── CMakeLists.txt
├── Makefile
├── dmicmp.dmr
diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt
new file mode 100644
index 0000000..f6e656a
--- /dev/null
+++ b/tools/CMakeLists.txt
@@ -0,0 +1,2 @@
+# Application-type DMOD modules that live under dmicmp/tools/.
+add_subdirectory(ping)
diff --git a/tools/ping/CMakeLists.txt b/tools/ping/CMakeLists.txt
new file mode 100644
index 0000000..979a833
--- /dev/null
+++ b/tools/ping/CMakeLists.txt
@@ -0,0 +1,56 @@
+# Name of the module
+set(DMOD_MODULE_NAME ping)
+
+# Author (should be string)
+set(DMOD_AUTHOR_NAME "Patryk Kubiak")
+
+# Stack size for the module (should be integer)
+set(DMOD_STACK_SIZE 1024)
+
+# Priority for the thread of the module (should be integer)
+set(DMOD_PRIORITY 0)
+
+# Path to the .dmr file - enables release package creation in build/packages/
+set(DMOD_DMR_PATH ${CMAKE_CURRENT_SOURCE_DIR}/ping.dmr)
+
+#
+# dmod_add_executable - create an executable module
+# it has the same signature as add_executable
+# and can be used in the same way after the creation
+# (for example, to link libraries)
+#
+dmod_add_executable(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION}
+ src/ping.c
+)
+
+# dmicmp is this same repository's own top-level module, already built as
+# part of this CMake configure by the time tools/ is added (see the root
+# CMakeLists.txt) - link its dmicmp_if target directly, exactly like
+# tests/CMakeLists.txt does, instead of fetching dmicmp's own published
+# headers back via dmod_link_modules() as if it were an external module.
+target_link_libraries(${DMOD_MODULE_NAME} dmicmp_if)
+
+# dmip: dmicmp.h #includes dmip.h directly (dmip_addr_t, dmip_send()'s
+# family) - needed for that header to resolve even though ping.c
+# itself never calls a dmip_* function (same reasoning dmicmp's own
+# CMakeLists.txt gives for linking dmroute/dmnetif header-only).
+# dmroute: dmip.h needs dmroute_addr_t (re-exported as dmip_addr_t)
+# directly - dmicmp_if alone doesn't carry dmroute's own include
+# dir.
+# dmnetif: dmip.h needs dmnetif_iface_t for dmip_protocol_handler_t: not
+# called from ping.c, just part of the header chain dmicmp.h
+# pulls in.
+# dmarp: DMARP_DEFAULT_TIMEOUT_MS, the ARP-resolution timeout forwarded to
+# dmicmp_v4_send_echo_request().
+# dmosi: dmosi_semaphore_*() to block for the matching Echo Reply (with a
+# timeout), dmosi_get_tick_count() for round-trip time, and
+# dmosi_thread_sleep() for the delay between requests.
+dmod_link_modules(${DMOD_MODULE_NAME}
+ dmip
+ dmroute
+ dmnetif
+ dmarp
+ dmosi
+)
+
+add_subdirectory(tests)
diff --git a/tools/ping/README.md b/tools/ping/README.md
new file mode 100644
index 0000000..99c298d
--- /dev/null
+++ b/tools/ping/README.md
@@ -0,0 +1,108 @@
+# ping
+
+[](../../LICENSE)
+
+`ping` DMOD application module - a CLI to send ICMPv4 Echo Requests to a
+host and report the matching Echo Replies, built on top of
+[dmicmp](../../)'s own Echo Request/Reply API. It builds/parses no ICMP
+bytes itself, only calls `dmicmp_v4_send_echo_request()` and
+`dmicmp_register_echo_listener()`/`_unregister_echo_listener()`.
+
+## Usage
+
+```
+ping
Send 4 ICMPv4 Echo Requests to
+ping --help | -h Show this help
+
+Options:
+ -c Number of Echo Requests to send (default: 4)
+ -i Delay between requests, in milliseconds (default: 1000)
+ -W Time to wait for a reply before it's lost, in milliseconds (default: 1000)
+ -s Number of payload bytes to send, 0-1024 (default: 32)
+```
+
+`` must be a literal dotted-decimal IPv4 address - dmicmp has no
+hostname resolution, and no ICMPv6 send path yet (see
+[include/dmicmp.h](../../include/dmicmp.h)'s top comment).
+
+Example output:
+
+```
+$ ping 192.168.1.1
+PING 192.168.1.1: 32 data bytes
+32 bytes from 192.168.1.1: icmp_seq=0 time=3 ms
+32 bytes from 192.168.1.1: icmp_seq=1 time=2 ms
+32 bytes from 192.168.1.1: icmp_seq=2 time=2 ms
+32 bytes from 192.168.1.1: icmp_seq=3 time=3 ms
+
+--- 192.168.1.1 ping statistics ---
+4 packets transmitted, 4 packets received, 0% packet loss
+```
+
+## Building
+
+This module lives under `tools/ping` inside the `dmicmp` repository and is
+built as part of the parent's CMake configure (the top-level
+`CMakeLists.txt` calls `add_subdirectory(tools)`, whose own
+`CMakeLists.txt` calls `add_subdirectory(ping)`) - it is not built
+standalone.
+
+```bash
+mkdir -p build
+cd build
+cmake ..
+cmake --build . --target ping
+```
+
+Pass `-DDMOD_DIR=/path/to/local/dmod` to build against a local dmod checkout
+instead of fetching `develop` from GitHub.
+
+It is also released as its own separate module-type application: any
+tagged release of this repository packages `ping` alongside `dmicmp`
+itself (see `build/packages/ping/` after a build with `DMOD_DMR_PATH` set,
+and the repository's release workflow).
+
+## Testing
+
+`tests/ping_test.c` drives this module through `Dmod_RunModule("ping",
+argc, argv)` (loads it, runs `main()`, unloads it again - see
+`Dmod_RunModule` in `dmod.h`) rather than depending on `ping` as a linked
+module: an Application-type module can't be loaded as another module's
+dependency (only a Library-type module can), and `Dmod_RunModule`
+sidesteps that entirely by loading/running/unloading it on demand instead
+of holding it resident for the test's lifetime.
+
+## Documentation
+
+See the `docs/` directory:
+
+- **[ping.md](docs/ping.md)** - Usage, exit codes, and how this module is
+ tested
+
+View documentation using `dmf-man ping`.
+
+## Project Structure
+
+```
+ping/
+├── docs/
+│ ├── README.md
+│ └── ping.md
+├── src/
+│ └── ping.c
+├── tests/
+│ ├── CMakeLists.txt
+│ └── ping_test.c
+├── CMakeLists.txt
+└── ping.dmr
+```
+
+LICENSE is shared with the rest of the `dmicmp` repository (`../../LICENSE`).
+
+## Author
+
+Patryk Kubiak
+
+## License
+
+MIT
diff --git a/tools/ping/docs/README.md b/tools/ping/docs/README.md
new file mode 100644
index 0000000..facd0d6
--- /dev/null
+++ b/tools/ping/docs/README.md
@@ -0,0 +1,27 @@
+# ping Documentation
+
+Welcome to the ping module documentation.
+
+## Contents
+
+- **[ping.md](ping.md)** - Usage, exit codes, and how this module is tested
+
+## Quick Reference
+
+```
+ping Send 4 ICMPv4 Echo Requests to
+ping --help | -h Show this help
+
+Options:
+ -c Number of Echo Requests to send (default: 4)
+ -i Delay between requests, in milliseconds (default: 1000)
+ -W Time to wait for a reply before it's lost, in milliseconds (default: 1000)
+ -s Number of payload bytes to send, 0-1024 (default: 32)
+```
+
+View documentation using `dmf-man`:
+
+```bash
+dmf-man ping # Main documentation
+dmf-man ping ping # ping.md
+```
diff --git a/tools/ping/docs/ping.md b/tools/ping/docs/ping.md
new file mode 100644
index 0000000..b729cea
--- /dev/null
+++ b/tools/ping/docs/ping.md
@@ -0,0 +1,114 @@
+# ping
+
+## Overview
+
+`ping` is a CLI over [dmicmp](../../..)'s Echo Request/Reply API - it sends
+one or more ICMPv4 Echo Requests to a literal IPv4 address and reports
+each matching Echo Reply, or that the request was lost. It never builds
+or parses an ICMP message itself; everything goes through
+`dmicmp_v4_send_echo_request()` to send and
+`dmicmp_register_echo_listener()`/`dmicmp_unregister_echo_listener()` to
+learn about the reply (see `include/dmicmp.h`).
+
+`` must be a literal dotted-decimal IPv4 address (e.g.
+`192.168.1.1`) - dmicmp has no hostname resolution, and no ICMPv6 send
+path yet (see `dmicmp.h`'s top comment), so `ping` cannot resolve a name
+or reach an IPv6 destination either.
+
+## Usage
+
+```
+ping Send 4 ICMPv4 Echo Requests to
+ping --help | -h Show this help
+
+Options:
+ -c Number of Echo Requests to send (default: 4)
+ -i Delay between requests, in milliseconds (default: 1000)
+ -W Time to wait for a reply before it's lost, in milliseconds (default: 1000)
+ -s Number of payload bytes to send, 0-1024 (default: 32)
+```
+
+Example output:
+
+```
+$ ping 192.168.1.1
+PING 192.168.1.1: 32 data bytes
+32 bytes from 192.168.1.1: icmp_seq=0 time=3 ms
+32 bytes from 192.168.1.1: icmp_seq=1 time=2 ms
+32 bytes from 192.168.1.1: icmp_seq=2 time=2 ms
+32 bytes from 192.168.1.1: icmp_seq=3 time=3 ms
+
+--- 192.168.1.1 ping statistics ---
+4 packets transmitted, 4 packets received, 0% packet loss
+```
+
+`time=` is measured locally, from just before
+`dmicmp_v4_send_echo_request()` is called to the moment the matching
+reply's listener callback posts the semaphore `ping` is waiting on
+(`dmosi_get_tick_count()` before/after) - it includes ARP resolution time
+for the very first request to a given host, the same way a real ping's
+first RTT sample often does.
+
+## How a request/reply round works
+
+Each Echo Request reuses the same ICMP identifier for the whole `ping`
+run (derived from the tick count at startup, so two concurrent `ping`
+invocations are unlikely to collide) but a fresh sequence number per
+request. Before sending, `ping`:
+
+1. Calls `dmicmp_register_echo_listener(identifier, echo_reply_handler)`.
+2. Calls `dmicmp_v4_send_echo_request(...)`.
+3. Blocks on a semaphore (created with `dmosi_semaphore_create(0, 1)`) for
+ up to `-W`'s timeout, via `dmosi_semaphore_wait()`.
+4. Calls `dmicmp_unregister_echo_listener(identifier)` unconditionally,
+ whether or not a reply arrived in time.
+
+`echo_reply_handler()` only posts the semaphore if the reply's sequence
+number matches the request currently outstanding - a reply that arrives
+late (after its own request already timed out and got unregistered) is
+simply never delivered at all, since `dmicmp` drops a reply for an
+identifier with no registered listener; the sequence check is an extra
+guard against the much narrower race where a *new* registration for the
+next request is already in place by the time a very late reply for the
+*previous* one arrives.
+
+## Exit codes
+
+- `0` - at least one Echo Request got a matching reply, or `--help`/`-h`
+ was given
+- `1` - bad arguments (missing/invalid address, unknown flag, an
+ out-of-range `-c`/`-W`/`-s` value), or every Echo Request was lost
+ (no reply within `-W`'s timeout, or `dmicmp_v4_send_echo_request()`
+ itself failed - e.g. no route to the destination)
+
+## Testing
+
+`tests/ping_test.c` drives this module through `Dmod_RunModule("ping",
+argc, argv)` (loads it, runs `main()`, unloads it again - see
+`Dmod_RunModule` in `dmod.h`) rather than depending on `ping` as a linked
+module: an Application-type module can't be loaded as another module's
+dependency (only a Library-type module can), and `Dmod_RunModule`
+sidesteps that entirely by loading/running/unloading it on demand instead
+of holding it resident for the test's lifetime.
+
+A "/dev/null"-backed `dmnetif` fixture, with a `dmroute` route and a
+`dmarp` cache entry pointed at it, lets a send be exercised up through
+`dmip_send()` itself - same pattern `dmicmp`'s own
+`tests/dmicmp_test.c` uses for `dmicmp_v4_send_error()`/
+`_send_dest_unreachable()`. No real driver backs the fixture, so
+`dmicmp_v4_send_echo_request()` still fails there (`dmnetif_send()` ->
+`-EIO`) and no real Echo Reply can ever arrive - every test step expects
+`ping` to report loss, not success; they only prove argument handling and
+a full send attempt complete without crashing or hanging, and report the
+honest outcome.
+
+## Dependencies
+
+- `dmicmp` - builds/sends the Echo Request and delivers the matching Echo
+ Reply (see the top-level [dmicmp](../../..) module)
+- `dmroute` - the address type (`dmroute_addr_t`, re-exported by `dmicmp`
+ as `dmip_addr_t`) `ping` parses `` into
+- `dmarp` - `DMARP_DEFAULT_TIMEOUT_MS`, the ARP-resolution timeout
+ forwarded to `dmicmp_v4_send_echo_request()`
+- `dmosi` - the semaphore `ping` waits for a reply on, the tick count used
+ to measure round-trip time, and the sleep between requests
diff --git a/tools/ping/ping.dmr b/tools/ping/ping.dmr
new file mode 100644
index 0000000..bd2e337
--- /dev/null
+++ b/tools/ping/ping.dmr
@@ -0,0 +1,45 @@
+# DMOD Resource File for ping
+# This file specifies where resources should be installed
+#
+# ping lives nested under dmicmp/tools/ping rather than being its own
+# top-level repo, so ${repo_dir}/${build_dir} below (which always resolve
+# to the dmicmp repo/build root, not this subdirectory) are prefixed with
+# tools/ping - same convention dmnet/tools/ifconfig/ifconfig.dmr uses.
+
+# === Core Module ===
+# Main module file - always installed
+dmf=./${module}.dmf => ${destination}/${module}.dmf [origin=${dmf_dir}/${module}.dmf]
+
+# Compressed module
+dmfc=./${module}.dmfc => ${destination}/${module}.dmfc [origin=${build_dir}/dmfc/${module}.dmfc]
+
+# Dependencies file (if exists)
+dmd=./${module}.dmd => ${destination}/${module}.dmd [origin=${dmf_dir}/${module}.dmd]
+
+# Version information
+version=./${module}_version.txt => ${destination}/${module}_version.txt [origin=${dmf_dir}/${module}_version.txt]
+
+# === Documentation ===
+# Module documentation in markdown format for dmf-man tool
+docs=./docs => ${destination}/${module}/docs [origin=${repo_dir}/tools/ping/docs]
+
+# README file
+readme=./README.md => ${destination}/${module}/README.md [origin=${repo_dir}/tools/ping/README.md]
+
+# === Header Files ===
+# No committed include/ dir of its own (its API is dmicmp's, see the
+# top-level dmicmp module) - just the generated module definitions header
+# every DMOD module gets, so dmf-man/consumers can still see it.
+# Destination must be a full *file* path (not a bare "include" dir) since
+# there's no separate directory-origin here to tell the packaging tool to
+# treat it as one - see dmicmp.dmr's inc= entry for the directory+file
+# combined case.
+inc=./include/${module}_defs.h => ${destination}/${module}/include/${module}_defs.h [origin=${build_dir}/tools/ping/${module}_defs.h]
+
+# === License ===
+# License file - shared at the dmicmp repo root (same "nested module
+# shares the parent repo's LICENSE" convention as dmnet/tools/ifconfig)
+license=./LICENSE => ${destination}/${module}/LICENSE [origin=${repo_dir}/LICENSE]
+
+# Adding itself
+dmr=./${module}.dmr => ${destination}/${module}/${module}.dmr [origin=${repo_dir}/tools/ping/${module}.dmr]
diff --git a/tools/ping/src/ping.c b/tools/ping/src/ping.c
new file mode 100644
index 0000000..51a8e3f
--- /dev/null
+++ b/tools/ping/src/ping.c
@@ -0,0 +1,289 @@
+/**
+ * @file ping.c
+ * @brief ping - send ICMPv4 Echo Requests to a host and report the replies
+ *
+ * A thin CLI over dmicmp's Echo Request/Reply API
+ * (dmicmp_v4_send_echo_request(), dmicmp_register_echo_listener()/
+ * _unregister_echo_listener() - see include/dmicmp.h). Registration is
+ * re-done for every request: the identifier is fixed for the whole run,
+ * but each iteration sets `g_expected_sequence` before sending and drops
+ * (via echo_reply_handler()'s own sequence check) any reply that doesn't
+ * match it - guards against a stale reply for an earlier request arriving
+ * after that request already timed out.
+ *
+ * must be a literal dotted-decimal IPv4 address - dmicmp has no
+ * hostname resolution, and no ICMPv6 send path yet (see dmicmp.h's top
+ * comment), so this tool cannot ping an IPv6 destination either.
+ */
+#include "dmod.h"
+#include "dmicmp.h"
+#include "dmroute.h"
+#include "dmarp.h"
+#include "dmosi.h"
+#include
+
+#define PING_DEFAULT_COUNT 4u
+#define PING_DEFAULT_INTERVAL_MS 1000u
+#define PING_DEFAULT_TIMEOUT_MS 1000u
+#define PING_DEFAULT_PAYLOAD_LEN 32u
+#define PING_MAX_PAYLOAD_LEN 1024u
+
+static dmosi_semaphore_t g_reply_sem;
+static uint16_t g_expected_sequence;
+static bool g_reply_matched;
+
+/* Parses a plain decimal uint32_t (dmod's minimal module runtime has no
+ * strtol()/atoi() - see dmod/src/module/string.c's replacement set). */
+static bool parse_uint32(const char* s, uint32_t* out)
+{
+ if (s == NULL || *s == '\0')
+ return false;
+
+ uint64_t value = 0;
+ for (const char* p = s; *p != '\0'; p++)
+ {
+ if (*p < '0' || *p > '9')
+ return false;
+
+ value = value * 10 + (uint64_t)(*p - '0');
+ if (value > UINT32_MAX)
+ return false;
+ }
+
+ *out = (uint32_t)value;
+ return true;
+}
+
+/* Parses "A.B.C.D" (each octet 0-255) into an IPv4 dmroute_addr_t. */
+static bool parse_ipv4_address(const char* s, dmroute_addr_t* ip)
+{
+ if (s == NULL)
+ return false;
+
+ ip->family = dmroute_family_v4;
+
+ for (int i = 0; i < DMROUTE_IPV4_ADDR_LEN; i++)
+ {
+ if (*s < '0' || *s > '9')
+ return false;
+
+ uint32_t octet = 0;
+ int digits = 0;
+ while (*s >= '0' && *s <= '9')
+ {
+ octet = octet * 10 + (uint32_t)(*s - '0');
+ if (octet > 255 || ++digits > 3)
+ return false;
+ s++;
+ }
+ ip->addr.v4[i] = (uint8_t)octet;
+
+ if (i < DMROUTE_IPV4_ADDR_LEN - 1)
+ {
+ if (*s != '.')
+ return false;
+ s++;
+ }
+ }
+
+ return *s == '\0';
+}
+
+/* Only ever called while a single dmicmp_register_echo_listener() /
+ * dmicmp_v4_send_echo_request() pair is outstanding (send_one_ping()
+ * sends requests one at a time, never concurrently) - g_expected_sequence
+ * and g_reply_matched are safe as plain statics under that single-writer
+ * invariant. */
+static void echo_reply_handler(const dmip_addr_t* src, uint16_t identifier, uint16_t sequence, const uint8_t* payload, size_t payload_len)
+{
+ (void)src;
+ (void)identifier;
+ (void)payload;
+ (void)payload_len;
+
+ if (sequence != g_expected_sequence)
+ return;
+
+ g_reply_matched = true;
+ dmosi_semaphore_post(g_reply_sem, 1);
+}
+
+/* Sends one Echo Request and blocks (up to wait_timeout_ms) for its
+ * reply. Returns 0 and prints a "bytes from ..." line if a matching reply
+ * arrived in time, -1 (and prints why) otherwise. */
+static int send_one_ping(const dmroute_addr_t* dst, const char* address_str, uint16_t identifier, uint16_t sequence,
+ const uint8_t* payload, size_t payload_len, uint32_t wait_timeout_ms)
+{
+ g_reply_matched = false;
+ g_expected_sequence = sequence;
+
+ int reg_ret = dmicmp_register_echo_listener(identifier, echo_reply_handler);
+ if (reg_ret != 0)
+ {
+ Dmod_Printf("ping: failed to register reply listener for icmp_seq=%u (error %d)\n", (unsigned)sequence, reg_ret);
+ return -1;
+ }
+
+ uint32_t start_tick = dmosi_get_tick_count();
+ int send_ret = dmicmp_v4_send_echo_request(dst, identifier, sequence, payload, payload_len, DMARP_DEFAULT_TIMEOUT_MS);
+ if (send_ret != 0)
+ {
+ dmicmp_unregister_echo_listener(identifier);
+ Dmod_Printf("ping: send failed for icmp_seq=%u (error %d)\n", (unsigned)sequence, send_ret);
+ return -1;
+ }
+
+ int wait_ret = dmosi_semaphore_wait(g_reply_sem, 1, (int32_t)wait_timeout_ms);
+ dmicmp_unregister_echo_listener(identifier);
+
+ if (wait_ret == 0 && g_reply_matched)
+ {
+ uint32_t rtt_ms = dmosi_get_tick_count() - start_tick;
+ Dmod_Printf("%u bytes from %s: icmp_seq=%u time=%u ms\n",
+ (unsigned)payload_len, address_str, (unsigned)sequence, (unsigned)rtt_ms);
+ return 0;
+ }
+
+ Dmod_Printf("Request timeout for icmp_seq %u\n", (unsigned)sequence);
+ return -1;
+}
+
+static void print_usage(const char* prog)
+{
+ Dmod_Printf("Usage:\n");
+ Dmod_Printf(" %s Send %u ICMPv4 Echo Requests to \n", prog, (unsigned)PING_DEFAULT_COUNT);
+ Dmod_Printf(" %s --help | -h Show this help\n", prog);
+ Dmod_Printf("\n");
+ Dmod_Printf("Options:\n");
+ Dmod_Printf(" -c Number of Echo Requests to send (default: %u)\n", (unsigned)PING_DEFAULT_COUNT);
+ Dmod_Printf(" -i Delay between requests, in milliseconds (default: %u)\n", (unsigned)PING_DEFAULT_INTERVAL_MS);
+ Dmod_Printf(" -W Time to wait for a reply before it's lost, in milliseconds (default: %u)\n", (unsigned)PING_DEFAULT_TIMEOUT_MS);
+ Dmod_Printf(" -s Number of payload bytes to send, 0-%u (default: %u)\n", (unsigned)PING_MAX_PAYLOAD_LEN, (unsigned)PING_DEFAULT_PAYLOAD_LEN);
+ Dmod_Printf("\n");
+ Dmod_Printf(" must be a literal IPv4 address (e.g. 192.168.1.1) - dmicmp has no\n");
+ Dmod_Printf("hostname resolution and no ICMPv6 send support yet.\n");
+}
+
+int main(int argc, char* argv[])
+{
+ const char* prog = (argc > 0 && argv[0] != NULL) ? argv[0] : "ping";
+
+ if (argc >= 2 && (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-h") == 0))
+ {
+ print_usage(prog);
+ return 0;
+ }
+
+ if (argc < 2)
+ {
+ print_usage(prog);
+ return 1;
+ }
+
+ dmroute_addr_t dst = { 0 };
+ if (!parse_ipv4_address(argv[1], &dst))
+ {
+ Dmod_Printf("%s: invalid IPv4 address '%s'\n", prog, argv[1]);
+ return 1;
+ }
+
+ uint32_t count = PING_DEFAULT_COUNT;
+ uint32_t interval_ms = PING_DEFAULT_INTERVAL_MS;
+ uint32_t wait_timeout_ms = PING_DEFAULT_TIMEOUT_MS;
+ uint32_t payload_len = PING_DEFAULT_PAYLOAD_LEN;
+
+ for (int i = 2; i < argc; i++)
+ {
+ if (strcmp(argv[i], "-c") == 0 && i + 1 < argc)
+ {
+ if (!parse_uint32(argv[++i], &count) || count == 0)
+ {
+ Dmod_Printf("%s: invalid count '%s'\n", prog, argv[i]);
+ return 1;
+ }
+ }
+ else if (strcmp(argv[i], "-i") == 0 && i + 1 < argc)
+ {
+ if (!parse_uint32(argv[++i], &interval_ms))
+ {
+ Dmod_Printf("%s: invalid interval '%s'\n", prog, argv[i]);
+ return 1;
+ }
+ }
+ else if (strcmp(argv[i], "-W") == 0 && i + 1 < argc)
+ {
+ if (!parse_uint32(argv[++i], &wait_timeout_ms) || wait_timeout_ms == 0)
+ {
+ Dmod_Printf("%s: invalid timeout '%s'\n", prog, argv[i]);
+ return 1;
+ }
+ }
+ else if (strcmp(argv[i], "-s") == 0 && i + 1 < argc)
+ {
+ if (!parse_uint32(argv[++i], &payload_len) || payload_len > PING_MAX_PAYLOAD_LEN)
+ {
+ Dmod_Printf("%s: invalid size '%s'\n", prog, argv[i]);
+ return 1;
+ }
+ }
+ else
+ {
+ print_usage(prog);
+ return 1;
+ }
+ }
+
+ uint8_t* payload = NULL;
+ if (payload_len > 0)
+ {
+ payload = Dmod_Malloc(payload_len);
+ if (payload == NULL)
+ {
+ Dmod_Printf("%s: out of memory allocating a %u-byte payload\n", prog, (unsigned)payload_len);
+ return 1;
+ }
+ for (uint32_t i = 0; i < payload_len; i++)
+ payload[i] = (uint8_t)i;
+ }
+
+ g_reply_sem = dmosi_semaphore_create(0, 1);
+ if (g_reply_sem == NULL)
+ {
+ Dmod_Printf("%s: failed to create the reply semaphore\n", prog);
+ if (payload != NULL)
+ Dmod_Free(payload);
+ return 1;
+ }
+
+ /* Ticks-derived rather than a fixed constant, so two `ping` runs in
+ * flight at once (e.g. against different hosts) are unlikely to share
+ * an identifier and cross-deliver each other's replies. */
+ uint16_t identifier = (uint16_t)(dmosi_get_tick_count() & 0xFFFFu);
+
+ Dmod_Printf("PING %s: %u data bytes\n", argv[1], (unsigned)payload_len);
+
+ uint32_t transmitted = 0;
+ uint32_t received = 0;
+
+ for (uint32_t seq = 0; seq < count; seq++)
+ {
+ transmitted++;
+ if (send_one_ping(&dst, argv[1], identifier, (uint16_t)seq, payload, payload_len, wait_timeout_ms) == 0)
+ received++;
+
+ if (seq + 1 < count)
+ dmosi_thread_sleep(interval_ms);
+ }
+
+ dmosi_semaphore_destroy(g_reply_sem);
+ g_reply_sem = NULL;
+ if (payload != NULL)
+ Dmod_Free(payload);
+
+ uint32_t loss_percent = (transmitted > 0) ? ((transmitted - received) * 100u) / transmitted : 0u;
+ Dmod_Printf("\n--- %s ping statistics ---\n", argv[1]);
+ Dmod_Printf("%u packets transmitted, %u packets received, %u%% packet loss\n",
+ (unsigned)transmitted, (unsigned)received, (unsigned)loss_percent);
+
+ return (received > 0) ? 0 : 1;
+}
diff --git a/tools/ping/tests/CMakeLists.txt b/tools/ping/tests/CMakeLists.txt
new file mode 100644
index 0000000..e39c20c
--- /dev/null
+++ b/tools/ping/tests/CMakeLists.txt
@@ -0,0 +1,23 @@
+set(DMOD_MODULE_NAME test_ping)
+set(DMOD_MODULE_VERSION ${PROJECT_VERSION})
+set(DMOD_AUTHOR_NAME "Patryk Kubiak")
+set(DMOD_STACK_SIZE 1024)
+set(DMOD_PRIORITY 0)
+
+dmod_add_test(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION}
+ ping_test.c
+)
+
+# `ping` itself is never linked as a dependency here - it's invoked on
+# demand through Dmod_RunModule() (see ping_test.c), which loads/runs/
+# unloads it per call instead of holding it resident for the whole test
+# run.
+# dmroute/dmarp: seed a fixture route and ARP cache entry so a send can be
+# exercised up to (but not through) a real driver, the same
+# pattern dmicmp's own tests/dmicmp_test.c uses.
+# dmnetif: the "/dev/null"-backed fixture interface the route points at.
+dmod_link_modules(${DMOD_MODULE_NAME}
+ dmnetif
+ dmroute
+ dmarp
+)
diff --git a/tools/ping/tests/ping_test.c b/tools/ping/tests/ping_test.c
new file mode 100644
index 0000000..28fd5c3
--- /dev/null
+++ b/tools/ping/tests/ping_test.c
@@ -0,0 +1,163 @@
+/**
+ * @file ping_test.c
+ * @brief Test steps for ping
+ *
+ * Drives the actual `ping` module through Dmod_RunModule("ping", argc,
+ * argv) (loads it, runs its main(), unloads it again - see
+ * Dmod_RunModule in dmod.h) rather than depending on `ping` as a linked
+ * module: an Application-type module can't be loaded as another module's
+ * dependency (only a Library-type module can) - same reasoning
+ * dmnet/tools/ifconfig/tests/ifconfig_test.c documents for itself.
+ *
+ * A "/dev/null"-backed dmnetif fixture, with a route and ARP cache entry
+ * pointed at it, lets a send be exercised up through dmip_send() the same
+ * way dmicmp's own tests/dmicmp_test.c does for dmicmp_v4_send_error()/
+ * _send_dest_unreachable() - no real driver backs it, so
+ * dmicmp_v4_send_echo_request() itself still fails (dmnetif_send() ->
+ * -EIO), and no real Echo Reply can ever arrive. Every step below
+ * therefore expects ping to report loss, not success - it only proves
+ * argument handling and the full send attempt don't crash and report the
+ * honest outcome, the same boundary ifconfig_test.c's "up"/"down" steps
+ * document for themselves.
+ */
+#include "dmod_test.h"
+#include "dmroute.h"
+#include "dmarp.h"
+#include
+
+#define TEST_DEVICE_PATH "/dev/null"
+
+static dmnetif_iface_t g_iface = NULL;
+static dmroute_route_t g_route = NULL;
+
+void dmod_test_setup(void)
+{
+ g_iface = dmnetif_register("test0", TEST_DEVICE_PATH);
+
+ dmroute_addr_t dest_net = { 0 };
+ dest_net.family = dmroute_family_v4;
+ dest_net.addr.v4[0] = 203; dest_net.addr.v4[1] = 0; dest_net.addr.v4[2] = 113; dest_net.addr.v4[3] = 0;
+
+ dmroute_addr_t netmask = { 0 };
+ netmask.family = dmroute_family_v4;
+ netmask.addr.v4[0] = 255; netmask.addr.v4[1] = 255; netmask.addr.v4[2] = 255; netmask.addr.v4[3] = 0;
+
+ g_route = dmroute_add(&dest_net, &netmask, NULL, "test0", DMROUTE_DEFAULT_METRIC, dmroute_origin_static);
+
+ dmroute_addr_t dst = { 0 };
+ dst.family = dmroute_family_v4;
+ dst.addr.v4[0] = 203; dst.addr.v4[1] = 0; dst.addr.v4[2] = 113; dst.addr.v4[3] = 7;
+
+ dmnetif_mac_addr_t fake_mac = { { 0x02, 0x00, 0x00, 0x00, 0x00, 0x40 } };
+ dmarp_cache_insert(g_iface, &dst, &fake_mac);
+}
+
+void dmod_test_teardown(void)
+{
+ dmroute_addr_t dst = { 0 };
+ dst.family = dmroute_family_v4;
+ dst.addr.v4[0] = 203; dst.addr.v4[1] = 0; dst.addr.v4[2] = 113; dst.addr.v4[3] = 7;
+ dmarp_cache_remove(g_iface, &dst);
+
+ dmroute_remove(g_route);
+ g_route = NULL;
+
+ dmnetif_unregister(g_iface);
+ g_iface = NULL;
+}
+
+DMOD_TEST_STEP(no_args_fails)
+{
+ char* argv[] = { "ping" };
+ DMOD_TEST_EXPECT_NE(Dmod_RunModule("ping", 1, argv), 0);
+}
+
+DMOD_TEST_STEP(run_module_unloads_ping_afterwards)
+{
+ char* argv[] = { "ping", "--help" };
+ Dmod_RunModule("ping", 2, argv);
+ DMOD_TEST_EXPECT_FALSE(Dmod_IsModuleLoaded("ping"));
+}
+
+DMOD_TEST_STEP(help_flag_succeeds)
+{
+ char* argv[] = { "ping", "--help" };
+ DMOD_TEST_EXPECT_EQ(Dmod_RunModule("ping", 2, argv), 0);
+}
+
+DMOD_TEST_STEP(short_help_flag_succeeds)
+{
+ char* argv[] = { "ping", "-h" };
+ DMOD_TEST_EXPECT_EQ(Dmod_RunModule("ping", 2, argv), 0);
+}
+
+DMOD_TEST_STEP(invalid_address_fails)
+{
+ char* argv[] = { "ping", "not-an-address" };
+ DMOD_TEST_EXPECT_NE(Dmod_RunModule("ping", 2, argv), 0);
+}
+
+DMOD_TEST_STEP(address_octet_out_of_range_fails)
+{
+ char* argv[] = { "ping", "192.168.1.999" };
+ DMOD_TEST_EXPECT_NE(Dmod_RunModule("ping", 2, argv), 0);
+}
+
+DMOD_TEST_STEP(unknown_flag_fails)
+{
+ char* argv[] = { "ping", "203.0.113.7", "--frobnicate" };
+ DMOD_TEST_EXPECT_NE(Dmod_RunModule("ping", 3, argv), 0);
+}
+
+DMOD_TEST_STEP(count_zero_fails)
+{
+ char* argv[] = { "ping", "203.0.113.7", "-c", "0" };
+ DMOD_TEST_EXPECT_NE(Dmod_RunModule("ping", 4, argv), 0);
+}
+
+DMOD_TEST_STEP(count_non_numeric_fails)
+{
+ char* argv[] = { "ping", "203.0.113.7", "-c", "many" };
+ DMOD_TEST_EXPECT_NE(Dmod_RunModule("ping", 4, argv), 0);
+}
+
+DMOD_TEST_STEP(timeout_zero_fails)
+{
+ char* argv[] = { "ping", "203.0.113.7", "-W", "0" };
+ DMOD_TEST_EXPECT_NE(Dmod_RunModule("ping", 4, argv), 0);
+}
+
+DMOD_TEST_STEP(size_over_max_fails)
+{
+ char* argv[] = { "ping", "203.0.113.7", "-s", "999999" };
+ DMOD_TEST_EXPECT_NE(Dmod_RunModule("ping", 4, argv), 0);
+}
+
+DMOD_TEST_STEP(unreachable_destination_reports_loss_without_crashing)
+{
+ /* No route anywhere in this file's fixtures covers this network. */
+ char* argv[] = { "ping", "198.51.100.1", "-c", "1", "-W", "50" };
+ DMOD_TEST_EXPECT_NE(Dmod_RunModule("ping", 5, argv), 0);
+}
+
+DMOD_TEST_STEP(routed_destination_without_real_driver_reports_loss)
+{
+ /* Route + ARP entry let dmicmp_v4_send_echo_request() reach
+ * dmnetif_send() itself, which fails (-EIO) since TEST_DEVICE_PATH is
+ * not a real driver - ping must still report the honest 100% loss
+ * outcome rather than crash or hang. */
+ char* argv[] = { "ping", "203.0.113.7", "-c", "2", "-i", "1", "-W", "50" };
+ DMOD_TEST_EXPECT_NE(Dmod_RunModule("ping", 7, argv), 0);
+}
+
+DMOD_TEST_STEP(custom_payload_size_is_accepted)
+{
+ char* argv[] = { "ping", "203.0.113.7", "-c", "1", "-s", "8", "-W", "50" };
+ DMOD_TEST_EXPECT_NE(Dmod_RunModule("ping", 7, argv), 0);
+}
+
+DMOD_TEST_STEP(zero_payload_size_is_accepted)
+{
+ char* argv[] = { "ping", "203.0.113.7", "-c", "1", "-s", "0", "-W", "50" };
+ DMOD_TEST_EXPECT_NE(Dmod_RunModule("ping", 7, argv), 0);
+}