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

OptionDefaultDescription
:timeout5000Timeout in milliseconds
:count1Number of pings (for ping_stats/2)
:payload_size56ICMP payload size in bytes
:max_concurrency50Max 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:

  1. Run as root (development/testing)

    sudo mix run -e 'RawPing.ping("8.8.8.8") |> IO.inspect'
  2. Set CAP_NET_RAW capability (Linux)

    sudo setcap cap_net_raw+ep /path/to/beam.smp
  3. Container 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:

LinuxmacOS/BSD
IP header on receivestrippedincluded
ICMP identifierrewritten by kernelpreserved
TTL availableno (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:

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

  1. Opens an ICMP socket, preferring the unprivileged datagram interface and falling back to a raw socket (:socket.open/3, with IPPROTO_ICMP given numerically so it does not depend on the system protocol database)
  2. Builds ICMP echo request packets with proper checksums
  3. Sends to target and receives replies with timeout handling
  4. 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.