https://github.com/zaxified/zig-fping
fping rewritten in Zig — reusable ICMP probing library + fping-compatible CLI. Pure Zig, no libc, built for 10k+ target monitoring.
https://github.com/zaxified/zig-fping
fping icmpv4 icmpv6 no-libc zig
Last synced: 3 days ago
JSON representation
fping rewritten in Zig — reusable ICMP probing library + fping-compatible CLI. Pure Zig, no libc, built for 10k+ target monitoring.
- Host: GitHub
- URL: https://github.com/zaxified/zig-fping
- Owner: zaxified
- License: other
- Created: 2026-06-11T04:29:34.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2026-06-19T10:20:19.000Z (about 1 month ago)
- Last Synced: 2026-06-19T12:17:04.262Z (about 1 month ago)
- Topics: fping, icmpv4, icmpv6, no-libc, zig
- Language: Zig
- Homepage:
- Size: 130 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
Awesome Lists containing this project
README
# zig-fping
[](https://github.com/zaxified/zig-fping/actions/workflows/ci.yml)
[fping](https://github.com/schweikert/fping) reimplemented in Zig (0.16+) as a
reusable library plus the `zfping` CLI.
> **Note:** This project is a Claude (Fable 5) authored, automated rewrite of
> fping, created for study purposes — the complete fping→Zig migration up to
> v0.1.1 was done in a single ~5 hour session. Development continues from
> there (v0.1.2+) in the same agentic style. Agentic/AI-assisted pull requests
> are welcome — especially ones that keep the port coherent with upstream
> fping (see [CONTRIBUTING.md](CONTRIBUTING.md)).
Built for high-volume ICMP monitoring (10k+ host checks per 5-minute cycle,
e.g. as a backend for Nagios-like systems) with an emphasis on net-storm
protection — bursting probes into one network branch saturates links/CPE and
produces false DOWN states.
Pure Zig, standard library only: no external dependencies, no C/ASM hooks,
no libc — the binary is fully static. Linux only, by design (direct
`std.os.linux` syscalls; even reverse DNS and timezone handling are
reimplemented in Zig instead of calling libc).
## Why Zig (vs. the C original)
This project doubles as a showcase of what Zig brings over the C tree
it was ported from:
- **Testing built into the language.** fping ships zero unit tests (only
a Perl end-to-end suite); here the parsers, checksum, seqmap, target
generator, option parser and even the scheduler's pacing math have unit
tests, and every test runs under a leak-detecting allocator.
- **Fuzzing built into the toolchain.** `scripts/fuzz.sh` (a time-boxed
`zig build test --fuzz --release=safe`) fuzzes the ICMP and DNS-response parsers — the
code paths that consume untrusted bytes from the network — plus the CLI
option parser and `-g` target generation. In C this needs external
harnesses (libFuzzer/AFL). The same fuzz bodies also run as smoke tests
in every `zig build test`.
- **Comptime.** The wire checksum is verified *at compile time* (see
`icmp.zig` "comptime checksum" test) — a regression cannot even build.
Tagged unions with exhaustive switches replace C's int-and-convention
error handling.
- **Memory safety in safe builds.** Bounds-checked slices instead of raw
pointer arithmetic over packet buffers; ReleaseSafe keeps the checks on
in production.
- **No autotools.** `build.zig` replaces configure.ac/Makefile.am, and
cross-compiling is just a target flag: `zig build -Dtarget=aarch64-linux
-Doptimize=ReleaseSafe` (CI builds aarch64, riscv64 and musl variants on
every push).
- **One static binary, package-manager distribution.** No libc at all;
consumers add the library with `zig fetch`.
- **Better algorithmic scaling.** Priority queues (O(log n)) replace
fping's sorted linked-list event queue (O(n) insert) — relevant at 10k+
concurrent targets.
## Performance vs. fping
Measured against fping 5.1, same isolated netns, 2000 loopback targets,
identical flags, ReleaseSafe build — regenerate any time with
`scripts/bench.sh`:
| Scenario | zfping | fping 5.1 |
| --- | --- | --- |
| `-c 1 -i 1` (pacing-bound) | 2.20 s, 3% CPU | 2.25 s, 3% CPU |
| `-c 1 -i 0.1` | 0.31 s, 11% CPU | 0.32 s, 15% CPU |
| `-c 3 -i 0.1 -p 100` | 0.94 s, 11% CPU | 0.95 s, 13% CPU |
| 250 silent targets `-i 0.1 -r 0 -t 100` | 0.14 s, 5% CPU | 0.14 s, 3% CPU |
Within noise of the original (slightly ahead in most runs), with ~1 MB
extra RSS for the fixed sequence-slot table. Since 0.1.1 the engine
batches syscalls: replies always drain via `recvmmsg` (16 packets per
syscall), and when pacing permits back-to-back sends (`-i 0`) consecutive
probes leave as one `sendmmsg` — with the default pacing the send path is
intentionally one packet per gap, keeping net-storm protection intact.
### Binary size
The released `zfping` is ~470 KB versus fping's ~52 KB — but fping is
dynamically linked against glibc (~2.2 MB shared), while zfping is fully
static with zero runtime dependencies. By symbol breakdown, the port's own
code is ~60 KB (on par with fping); the rest is the statically linked
"libc equivalent" from Zig's std: the I/O runtime with the DNS resolver,
panic/stack-trace machinery (which C has no equivalent of), formatting and
allocator code.
The build is intentionally **ReleaseSafe**: bounds checks stay enabled on
the code paths that parse untrusted network bytes (ICMP and DNS replies),
which is part of this port's value over the C original. The checks cost
~55 KB of the binary and no measurable speed (the benchmark above runs
with them on). For embedded targets with tight flash budgets,
`zig build -Doptimize=ReleaseSmall -Dstrip=true` produces a ~248 KB
binary — at the price of disabling those safety checks, so prefer
ReleaseSafe wherever size is not critical.
## Net-storm protection
| Mechanism | Config | fping equivalent |
| --- | --- | --- |
| Global minimum gap between any two packets | `interval_ns` (default 10 ms) | `-i` |
| Gap between probes to the same target | `perhost_interval_ns` | `-p` |
| Cap on concurrently outstanding probes | `max_inflight` (default 4096) | — |
| Minimum gap into the same /24 (v4) or /64 (v6) | `subnet_gap_ns` | — |
| Random jitter of each target's first probe (cycle decorrelation) | `jitter_ns` | — |
| Exponential timeout backoff on retries | `backoff_factor` | `-B` |
Concurrency lives in the 16-bit ICMP sequence space (one socket per address
family, seq → (target, probe) mapping like fping's seqmap), not in file
descriptor counts — the loop waits in `ppoll` on at most two sockets.
10 000 responding targets at a 0.05 ms interval complete in ~1.3 s
(single-threaded, ~15k packets/s including replies); a typical monitoring
cycle of 10k checks per 5 minutes uses well under 1% of that.
## Using the library
```sh
zig fetch --save git+https://github.com/zaxified/zig-fping
```
```zig
// build.zig
const fping_dep = b.dependency("zig_fping", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("fping", fping_dep.module("zig_fping"));
```
```zig
// use inside your code
const std = @import("std");
const fping = @import("fping");
var pinger = try fping.Pinger.init(allocator, .{
.mode = .count, // .alive = stop after first reply (host check)
.count = 3, // probes per target
.interval_ns = 5 * std.time.ns_per_ms,
.subnet_gap_ns = 2 * std.time.ns_per_ms,
.jitter_ns = 500 * std.time.ns_per_ms,
});
defer pinger.deinit();
const id = try pinger.addTarget("192.0.2.1");
// optional: pinger.setResultCallback(ctx, onResult); — per-probe outcomes
try pinger.run(); // blocks until all probes complete
const st = pinger.stats(id);
// st.alive(), st.lossPermille(), st.avgNs(), st.min_ns, st.max_ns, ...
```
Modes:
- **`.alive`** — fping's default: one probe, retry on timeout (`retries`,
default 3) with backoff, done on first reply. Cheapest host-alive check.
- **`.count`** — fping `-c`: exactly `count` probes per target, full RTT/loss
statistics (perf data for monitoring).
- **`.loop`** — fping `-l`: probe forever until `stop()` is called
(async-signal-safe, callable from a signal handler or another thread).
`run()` can be called repeatedly (statistics reset) — typically once per
monitoring cycle.
Threading: the engine is fully single-threaded and spawns no threads — one
event loop multiplexing the 16-bit ICMP sequence space over at most two
sockets. A `Pinger` instance must be driven by a single thread; the only
cross-thread entry point is `stop()` (atomic, async-signal-safe). For
parallelism, run independent `Pinger` instances (each owns its sockets and
buffers) — though one instance saturates typical monitoring workloads.
For integration into an existing event loop, skip `run()` and drive the
engine yourself — `step()` never blocks:
```zig
try pinger.prepare();
while (try pinger.step()) |deadline_mono_ns| {
var buf: [2]std.os.linux.pollfd = undefined;
const fds = pinger.pollFds(&buf);
// poll/epoll fds until readable or the deadline, then step() again
}
```
## CLI
`zfping` is a thin shell over the library, compatible with fping options,
output formats and exit codes (`-c/-C/-l/-g/-f/-Q/-J/-N/-x/-X`,
`--icmp-timestamp`, `--check-source`, `--print-ttl/tos`, DNS targets, …).
Prebuilt static binaries (x86_64, aarch64, riscv64 — no libc required, run
anywhere) are published on the
[Releases page](https://github.com/zaxified/zig-fping/releases):
```sh
# adjust the version to the latest release tag
curl -fsSL -o zfping.tar.gz \
https://github.com/zaxified/zig-fping/releases/download/v0.1.2/zfping-v0.1.2-x86_64-linux.tar.gz
tar xzf zfping.tar.gz && ./zfping --help
```
Or build from source:
```sh
zig build -Doptimize=ReleaseSafe # same build mode as the released binaries
./zig-out/bin/zfping -c 3 -i 5 192.0.2.1 2001:db8::1
./zig-out/bin/zfping -g 192.168.1.0/24 -a -q
./zig-out/bin/zfping --help
```
Known divergences from fping are listed in [CHANGELOG.md](CHANGELOG.md)
and commented at the relevant places in the code.
The package also exports small standalone modules: `fping.rdns` (pure-Zig
reverse DNS/PTR client), `fping.LocalTz` (/etc/localtime offsets) and
`fping.netutil` (interface name→index).
## Permissions
The library tries an unprivileged `SOCK_DGRAM` ICMP socket first, then falls
back to `SOCK_RAW`:
```sh
# unprivileged ping (recommended for monitoring daemons):
sudo sysctl -w net.ipv4.ping_group_range="0 2147483647"
# or grant the binary the raw-socket capability:
sudo setcap cap_net_raw+ep zfping
```
Root-less testing works in an isolated namespace:
```sh
unshare -Urn sh -c 'ip link set lo up; ./zig-out/bin/zfping 127.0.0.1 ::1'
```
## Development
Everything works from a plain `git clone`, without sudo and without a
pre-installed Zig — the pinned toolchain (`.zigversion`, 0.16.0) is
downloaded automatically into `.zig-toolchain/` when missing:
```sh
scripts/test.sh # fmt check + build + unit + functional + golden-diff
scripts/release.sh # the above + static binaries for all targets
# into releases/v/ (tar.gz + SHA256SUMS)
scripts/fuzz.sh # time-boxed fuzzing of the parser fuzz targets
```
Or directly with your own Zig 0.16.0:
```sh
zig build -Doptimize=ReleaseSafe # library + zfping (shipped build mode)
zig build test -Doptimize=ReleaseSafe # unit tests
sh test/functional.sh # functional suite (isolated user+net namespace)
sh test/golden.sh # byte-diff against an installed reference fping
```
The GitHub release workflow runs the same `scripts/release.sh`, so local
and published artifacts are identical.
## Mapping to fping sources
| fping | zig-fping |
| --- | --- |
| `socket4.c` / `socket6.c` | `src/socket.zig` |
| `seqmap.c` | `src/seqmap.zig` |
| ICMP build/parse in `fping.c` | `src/icmp.zig` |
| `main_loop()` in `fping.c` | `src/pinger.zig` (priority queues instead of linked lists) |
| CLI orchestration (arg handling / setup in `fping.c`) | `src/main.zig` |
| option parsing (`optparse.c`) | `src/cli/options.zig` |
| target generation `-g` (`add_cidr`/`add_range`) | `src/cli/generate.zig` |
| per-probe & interval output (`output.c`) | `src/cli/output.zig` |
| final statistics (`stats.c`) | `src/cli/stats.zig` |
| libc `getnameinfo` (reverse DNS) | `src/rdns.zig` (pure-Zig PTR client) |
| libc `localtime` | `src/tzlocal.zig` (/etc/localtime via std.tz) |
## License
Derived from fping; the original fping license applies — see
[LICENSE](LICENSE).