RawPing
Pure Erlang/OTP ICMP ping library using the modern :socket API.
No NIFs, no external dependencies, no debug trace memory leaks. Requires Elixir 1.17+ (OTP 25+).
Installation
Add raw_ping to your list of dependencies in mix.exs:
def deps do
[
{:raw_ping, "~> 0.3"}
]
end
Usage
# Single ping - returns RTT in milliseconds
{:ok, rtt} = RawPing.ping("8.8.8.8")
{:ok, rtt} = RawPing.ping({8, 8, 8, 8})
{:ok, rtt} = RawPing.ping([8, 8, 8, 8]) # list format also works
# With options
{:ok, rtt} = RawPing.ping("8.8.8.8", timeout: 2000)
{:error, :timeout} = RawPing.ping("192.0.2.1", timeout: 100)
# Multiple pings with statistics
{:ok, stats} = RawPing.ping_stats("8.8.8.8", count: 5)
# %{min: 10.2, max: 15.8, avg: 12.5, success_rate: 1.0, success_count: 5, failure_count: 0, rtts: [...]}
# Batch ping multiple hosts concurrently
results = RawPing.ping_batch(["8.8.8.8", "1.1.1.1", "192.168.1.1"], timeout: 1000)
# %{"8.8.8.8" => {:ok, 12.5}, "1.1.1.1" => {:ok, 8.2}, "192.168.1.1" => {:error, :timeout}}
Options
| Option | Default | Description |
|---|---|---|
:timeout | 5000 | Timeout in milliseconds |
:count | 1 | Number of pings (for ping_stats/2) |
:payload_size | 56 | ICMP payload size in bytes |
:max_concurrency | 50 | Max concurrent pings (for ping_batch/2) |
Hostnames
Targets may be hostnames or literal addresses:
RawPing.ping("example.com")
RawPing.ping("8.8.8.8")
RawPing.ping({8, 8, 8, 8})
Names are resolved per call, so a target that starts pointing somewhere else is
followed rather than pinned to whatever it resolved to at startup. A name that
does not resolve returns {:error, :nxdomain}.
IPv4 only: the socket family is fixed at :inet, so IPv6 targets are not yet
supported.
Privileges
Most hosts need none. By default RawPing opens an unprivileged ICMP datagram
socket (SOCK_DGRAM/IPPROTO_ICMP), falling back to a raw socket only if that
is unavailable.
Check which interface you got:
RawPing.socket_mode()
#=> {:ok, :dgram} # unprivileged
#=> {:ok, :raw} # fell back; needed root or CAP_NET_RAW
On Linux, datagram ICMP is gated by net.ipv4.ping_group_range, which must
include the running process's GID. Many distributions already ship it wide open:
$ cat /proc/sys/net/ipv4/ping_group_range
0 2147483647 # any GID may use unprivileged ICMP
If it is restrictive, either widen it — no runtime capability required:
sudo sysctl -w net.ipv4.ping_group_range="0 2147483647"
…or grant the raw-socket path a privilege, as before:
Run as root (development/testing)
sudo mix run -e 'RawPing.ping("8.8.8.8") |> IO.inspect'Set CAP_NET_RAW capability (Linux)
sudo setcap cap_net_raw+ep /path/to/beam.smpContainer with NET_RAW (Docker/Kubernetes)
securityContext:capabilities:add: ["NET_RAW"]
Note that a rootless container sharing the host network namespace cannot
obtain CAP_NET_RAW at all — that namespace is owned by the initial user
namespace, so the capability has no force there. In that deployment shape the
datagram path is the only way ICMP works.
When neither interface is available
If the datagram path is forbidden and the raw path lacks privileges, every call returns a clean error rather than crashing:
RawPing.socket_mode() #=> {:error, :permission_denied}
RawPing.ping("8.8.8.8") #=> {:error, :permission_denied}
RawPing.ping_stats(...) #=> {:error, :permission_denied}
Check for this at startup rather than per-ping. A monitoring tool that treats every error as "host unreachable" will report all of its targets as down, and the real cause — that it cannot open a socket at all — looks identical to a total outage. That is a bad page to receive at 3am.
case RawPing.socket_mode() do
{:ok, mode} ->
Logger.info("ICMP available via #{mode} socket")
{:error, reason} ->
# Loud, and distinct from "the hosts are down"
Logger.error("ICMP unavailable: #{inspect(reason)}. Check net.ipv4.ping_group_range, "
<> "or grant CAP_NET_RAW.")
end
This is most likely to bite in a rootless container where ping_group_range has
been tightened, since CAP_NET_RAW is unobtainable there as a fallback.
Platform differences
The two platforms behave differently on datagram sockets. RawPing handles both by inspecting the reply rather than assuming a format:
| Linux | macOS/BSD | |
|---|---|---|
| IP header on receive | stripped | included |
| ICMP identifier | rewritten by kernel | preserved |
| TTL available | no (nil) | yes |
Because Linux rewrites the identifier, replies from a datagram socket are matched on sequence. Raw sockets keep the stricter id-and-sequence match, since they see every ICMP packet on the host.
Why Not gen_icmp?
This library was created as an alternative to gen_icmp which:
- Uses NIFs via
procketfor raw socket access - Abuses
gen_udpinternals in ways that can trigger debug traces - Can cause severe memory leaks (20GB+) when pinging unreachable hosts at scale
RawPing uses Erlang/OTP's native :socket API (available since OTP 22) which provides clean, safe access to raw sockets without any of these issues.
How It Works
- Opens an ICMP socket, preferring the unprivileged datagram interface and
falling back to a raw socket (
:socket.open/3, withIPPROTO_ICMPgiven numerically so it does not depend on the system protocol database) - Builds ICMP echo request packets with proper checksums
- Sends to target and receives replies with timeout handling
- Parses ICMP echo replies, filtering by ID/sequence to handle concurrent pings
Testing
# Run non-privileged tests (packet building/parsing)
mix test --exclude privileged
# Run all tests (requires sudo)
sudo mix test
License
MIT License - see LICENSE for details.